Skip to content

Commit

Permalink
Further improvements made mainly plain image Z stack support
Browse files Browse the repository at this point in the history
  • Loading branch information
folterj committed Jan 26, 2024
1 parent b4894ca commit 13372fa
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 67 deletions.
9 changes: 5 additions & 4 deletions OmeSliCC/OmeSource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class OmeSource:
"""OME-compatible image source (base class)"""
"""Internal image format is [TCZYX]"""

metadata: dict
"""metadata dictionary"""
Expand Down Expand Up @@ -206,7 +207,7 @@ def get_shape(self, dimension_order: str = None, xyzct: tuple = None) -> tuple:
def get_thumbnail(self, target_size: tuple, precise: bool = False) -> np.ndarray:
size, index = get_best_size(self.sizes, target_size)
scale = np.divide(target_size, self.sizes[index])
image = self.get_redimension_image(self._asarray_level(index))
image = self.get_yxc_image(self._asarray_level(index))
if precise:
thumbnail = precise_resize(image, scale)
else:
Expand Down Expand Up @@ -237,7 +238,7 @@ def get_window_min_max(self, channeli):
min, max = start, end
return {'start': start, 'end': end, 'min': min, 'max': max}

def get_redimension_image(self, image, t=0, z=0, c=None):
def get_yxc_image(self, image, t=0, z=0, c=None):
image = np.moveaxis(image, 1, -1)
if z is not None:
image = image[:, z, ...]
Expand All @@ -247,9 +248,9 @@ def get_redimension_image(self, image, t=0, z=0, c=None):
image = image[..., c]
return image

def render(self, image: np.ndarray, channels: list = []) -> np.ndarray:
def render(self, image: np.ndarray, t: int = 0, z: int = 0, channels: list = []) -> np.ndarray:
if image.ndim == 5:
image = self.get_redimension_image(image)
image = self.get_yxc_image(image, t=t, z=z)
new_image = np.zeros(list(image.shape[:2]) + [3], dtype=np.float32)
tot_alpha = 0
n = len(self.channels)
Expand Down
60 changes: 37 additions & 23 deletions OmeSliCC/PlainImageSource.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import numpy as np
from PIL import Image
from concurrent.futures import ThreadPoolExecutor
from tifffile import RESUNIT

from OmeSliCC.OmeSource import OmeSource
from OmeSliCC.image_util import *
Expand All @@ -23,24 +22,17 @@ def __init__(self,
filename: str,
source_pixel_size: list = None,
target_pixel_size: list = None,
source_info_required: bool = False,
executor: ThreadPoolExecutor = None):
source_info_required: bool = False):

super().__init__()
self.loaded = False
self.arrays = []

if executor is not None:
self.executor = executor
else:
max_workers = (os.cpu_count() or 1) + 4
self.executor = ThreadPoolExecutor(max_workers)

self.image = Image.open(filename)
self.metadata = get_pil_metadata(self.image)
size = (self.image.width, self.image.height)
self.sizes = [size]
nchannels = self.image.im.bands
nchannels = len(self.image.getbands())
size_xyzct = (self.image.width, self.image.height, self.image.n_frames, nchannels, 1)
self.sizes_xyzct = [size_xyzct]
pixelinfo = pilmode_to_pixelinfo(self.image.mode)
Expand All @@ -61,25 +53,38 @@ def __init__(self,

def _find_metadata(self):
self.source_pixel_size = []
pixel_size_unit = self.metadata.get('unit', '')
pixel_size_unit = None
pixel_size_z = None

description = self.metadata.get('ImageDescription', '')
if description != '':
metadata = desc_to_dict(description)
if 'spacing' in metadata:
pixel_size_unit = metadata.get('unit', '')
if not isinstance(pixel_size_unit, str):
pixel_size_unit = 'micrometer'
pixel_size_z = metadata['spacing']
if not pixel_size_unit:
pixel_size_unit = self.metadata.get('ResolutionUnit')
if pixel_size_unit is not None:
pixel_size_unit = str(RESUNIT(pixel_size_unit).name).lower()
if pixel_size_unit == 'none':
pixel_size_unit = ''
res0 = self.metadata.get('XResolution')
if res0 is not None:
if isinstance(res0, tuple):
res0 = res0[0] / res0[1]
if res0 != 0:
self.source_pixel_size.append((1 / res0, pixel_size_unit))
self.source_pixel_size.append((1 / float(res0), pixel_size_unit))
res0 = self.metadata.get('YResolution')
if res0 is not None:
if isinstance(res0, tuple):
res0 = res0[0] / res0[1]
if res0 != 0:
self.source_pixel_size.append((1 / res0, pixel_size_unit))
self.source_pixel_size.append((1 / float(res0), pixel_size_unit))
if pixel_size_z is not None:
self.source_pixel_size.append((pixel_size_z, pixel_size_unit))
self.source_mag = self.metadata.get('Mag', 0)
self.channels = [{'label': ''}]

def load(self):
self.unload()
self.arrays.append(np.array(self.image))
for level in range(len(self.sizes)):
self.arrays.append(self._asarray_level(level))
self.loaded = True

def unload(self):
Expand All @@ -93,15 +98,24 @@ def _asarray_level(self, level: int, x0: float = 0, y0: float = 0, x1: float = -
if x1 < 0 or y1 < 0:
x1, y1 = self.sizes[level]

nframes = self.image.n_frames
if self.loaded:
image = self.arrays[level]
elif nframes > 1:
shape = [nframes] + list(np.array(self.image).shape)
image = np.zeros(shape, dtype=self.pixel_types[level])
for framei in range(nframes):
self.image.seek(framei)
image[framei] = np.array(self.image)
else:
image = np.array(self.image)

if 'z' not in self.dimension_order:
image = np.expand_dims(image, 0) # add Z
if 'c' in self.dimension_order:
image = np.moveaxis(image, -1, 0) # move C to front
else:
image = np.expand_dims(image, 0) # add C
image = np.expand_dims(image, 0) # add T
if image.ndim < 5:
image = np.expand_dims(image, 2) # add Z
image = image[..., y0:y1, x0:x1]
return image
15 changes: 8 additions & 7 deletions OmeSliCC/TiffSource.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,14 @@ def _asarray_level(self, level: int, x0: float = 0, y0: float = 0, x1: float = -
xyzct[0] = w
xyzct[1] = h

# Match internal Tiff page dimensions: separate sample, depth, length, width, contig sample
s = self.npages
# Match internal Tiff page dimensions [separate sample, depth, length, width, contig sample]
n = self.npages
d = self.depth
n = nc
if n > 1 and n == self.npages:
n = 1
shape = s, d, h, w, n
s = nc
if self.npages == s > 1:
# in case channels represented as pages
s = 1
shape = n, d, h, w, s
out = np.zeros(shape, page.dtype)

dataoffsets = []
Expand All @@ -297,7 +298,7 @@ def _asarray_level(self, level: int, x0: float = 0, y0: float = 0, x1: float = -
target_y0 = y0 - tile_y0 * tile_height
target_x0 = x0 - tile_x0 * tile_width
image = out[:, :, target_y0: target_y0 + dh, target_x0: target_x0 + dw, :]
# 'sdyxn' -> 'tzyxc'
# 'ndyxs' -> 'tzyxc'
if image.shape[0] == nc > 1:
image = np.swapaxes(image, 0, -1)
elif image.shape[0] == nz > 1:
Expand Down
32 changes: 16 additions & 16 deletions OmeSliCC/image_util.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import cv2 as cv
import imagecodecs
from imagecodecs.numcodecs import Lzw, Jpeg2k, Jpegxr, Jpegxl
from numcodecs import register_codec
import PIL.Image
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
import tifffile
import PIL.Image
from PIL.ExifTags import TAGS
import imagecodecs
import tifffile
from tifffile import TiffFile

from OmeSliCC.util import *
Expand Down Expand Up @@ -34,15 +34,15 @@ def show_image_gray(image: np.ndarray):
plt.show()


def int2float_image(image):
def int2float_image(image: np.ndarray) -> np.ndarray:
if image.dtype.kind != 'f':
maxval = 2 ** (8 * image.dtype.itemsize) - 1
return image / np.float32(maxval)
else:
return image


def float2int_image(image, dtype=np.dtype(np.uint8)):
def float2int_image(image: np.ndarray, dtype: np.dtype = np.dtype(np.uint8)) -> np.ndarray:
if not (image.dtype.kind == 'i' or image.dtype.kind == 'u') and not dtype.kind == 'f':
maxval = 2 ** (8 * dtype.itemsize) - 1
return (image * maxval).astype(dtype)
Expand Down Expand Up @@ -81,12 +81,12 @@ def convert_image_sign_type(image0: np.ndarray, dtype: np.dtype) -> np.ndarray:
return image


def get_image_quantile(image, quantile, axis=None):
def get_image_quantile(image: np.ndarray, quantile: float, axis=None) -> float:
value = np.quantile(image, quantile, axis=axis).astype(image.dtype)
return value


def normalise_values(image, min_value, max_value):
def normalise_values(image: np.ndarray, min_value: float, max_value: float) -> np.ndarray:
return np.clip((image.astype(np.float32) - min_value) / (max_value - min_value), 0, 1)


Expand Down Expand Up @@ -208,7 +208,7 @@ def precise_resize(image: np.ndarray, scale: np.ndarray) -> np.ndarray:
return new_image.astype(image.dtype)


def create_compression_filter(compression):
def create_compression_filter(compression: list) -> tuple:
compressor, compression_filters = None, None
compression = ensure_list(compression)
if compression is not None and len(compression) > 0:
Expand Down Expand Up @@ -254,6 +254,13 @@ def get_tiff_pages(tiff: TiffFile) -> list:
return pages


def tags_to_dict(tags: tifffile.TiffTags) -> dict:
tag_dict = {}
for tag in tags.values():
tag_dict[tag.name] = tag.value
return tag_dict


def tiff_info(filename: str) -> str:
s = ''
nom_size = 0
Expand Down Expand Up @@ -360,13 +367,6 @@ def calc_fraction_used(image: np.ndarray, threshold: float = 0.1) -> float:
return fraction


def reverse_last_axis(image, reverse=True):
if reverse and len(image.shape) > 2 and image.shape[-1] > 1:
return np.moveaxis(image, -1, 0)
else:
return image


def save_image(image: np.ndarray, filename: str, output_params: dict = {}):
compression = output_params.get('compression')
PIL.Image.fromarray(image).save(filename, compression=compression)
2 changes: 1 addition & 1 deletion OmeSliCC/ome_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def create_ome_metadata(source: OmeSource,
image0 = {}
pixels0 = image0.get('Pixels', {})

nchannels = source.get_size_xyzct()[3]
nchannels = source.get_nchannels()
channels0 = source.get_channels()
n = len(channels0)
samples_per_pixel = nchannels // n
Expand Down
2 changes: 1 addition & 1 deletion OmeSliCC/omero_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def create_ome_metadata_from_omero(source: OmeSource,
}
channels.append(channel)

nchannels = source.get_size_xyzct()[3]
nchannels = source.get_nchannels()
if combine_rgb and len(channels) == 3:
channel = channels[0].copy()
channel['@SamplesPerPixel'] = nchannels
Expand Down
20 changes: 6 additions & 14 deletions OmeSliCC/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os
import re
import numpy as np
import tifffile


def get_default(x, default):
Expand All @@ -18,7 +17,7 @@ def ensure_list(x) -> list:
return [x]


def filter_dict(dict0):
def filter_dict(dict0: dict) -> dict:
new_dict = {}
for key, value0 in dict0.items():
if value0 is not None:
Expand All @@ -33,13 +32,6 @@ def filter_dict(dict0):
return new_dict


def tags_to_dict(tags: tifffile.TiffTags) -> dict:
tag_dict = {}
for tag in tags.values():
tag_dict[tag.name] = tag.value
return tag_dict


def desc_to_dict(desc: str) -> dict:
desc_dict = {}
if desc.startswith('{'):
Expand All @@ -66,7 +58,7 @@ def desc_to_dict(desc: str) -> dict:
return desc_dict


def print_dict(dct: dict, indent: int = 0):
def print_dict(dct: dict, indent: int = 0) -> str:
s = ''
for key, value in dct.items():
s += '\n'
Expand Down Expand Up @@ -116,7 +108,7 @@ def get_filetitle(filename: str) -> str:
return os.path.splitext(filebase)[0]


def split_num_text(text):
def split_num_text(text: str) -> list:
num_texts = []
block = ''
is_num0 = None
Expand Down Expand Up @@ -145,7 +137,7 @@ def split_num_text(text):
return num_texts2


def split_value_unit_list(text):
def split_value_unit_list(text: str) -> list:
value_units = []
if text is None:
return None
Expand Down Expand Up @@ -173,7 +165,7 @@ def split_value_unit_list(text):
return value_units


def get_value_units_micrometer(value_units0: list):
def get_value_units_micrometer(value_units0: list) -> list:
conversions = {'nm': 1e-3, 'µm': 1, 'um': 1, 'micrometer': 1, 'mm': 1e3, 'cm': 1e4, 'm': 1e6}
if value_units0 is None:
return None
Expand All @@ -188,7 +180,7 @@ def get_value_units_micrometer(value_units0: list):
return values_um


def convert_rational_value(value):
def convert_rational_value(value) -> float:
if value is not None and isinstance(value, tuple):
value = value[0] / value[1]
return value
2 changes: 1 addition & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


def render_at_pixel_size(filename: str, source_pixel_size: list = None,
target_pixel_size: list = None, pos: tuple = (0, 0, -1, -1)) -> np.ndarray:
target_pixel_size: list = None, pos: tuple = (0, 0, -1, -1)) -> np.ndarray:
if filename.endswith('.zarr'):
source = OmeZarrSource(filename, source_pixel_size)
else:
Expand Down

0 comments on commit 13372fa

Please sign in to comment.