import cv2
import pandas as pd
import numpy as np
import os
from PIL import Image
import tifffile as tf
import imageio as im
from .process_steps import Reader, Converter, Preprocessor,\
Binarization, Postprocessor, FeatureFinder, FeatureFilter
from .parameters import SpotlobParameterSet, EnumParameter,\
BoolParameter, NumericRangeParameter
[docs]class SimpleReader(Reader):
"""Reads an image from a file as an RGB file.
Standard image formats, such as `png`, `jpg`, `tif` are supported.
It uses `cv2.imread`.
"""
def __init__(self):
super(SimpleReader, self).__init__(self.fn_read, [])
def fn_read(self, filepath):
# load as color, convert from BGR to RGB
if os.path.exists(filepath):
return cv2.cvtColor(cv2.imread(filepath), cv2.COLOR_BGR2RGB), {}
else:
raise IOError("File %s not found" % filepath)
[docs]class TifReader(Reader):
"""
Reads an image from a file as an RGB file.
Only image format `tif` is supported.
It uses `tifffile.memmap`.
"""
def __init__(self):
pars = [NumericRangeParameter("width_percent", 100, 0, 100),
NumericRangeParameter("height_percent", 100, 0, 100),
NumericRangeParameter("x0_percent", 0, 0, 100),
NumericRangeParameter("y0_percent", 0, 0, 100)]
self._preview_image_cache = None
super(TifReader, self).__init__(self.partial_read, pars)
def _absolute_ROI_corners(self, filepath, width_percent, height_percent, x0, y0):
if os.path.exists(filepath):
im = Image.open(filepath)
w, h = im.size
new_w = int(w * width_percent / 100)
new_h = int(h * height_percent / 100)
x0 = int(w * x0 / 100)
y0 = int(h * y0 / 100)
x1 = x0 + new_w
y1 = y0 + new_h
return x0, y0, min(x1,w), min(y1,h)
else:
raise IOError(f"File {filepath} not found")
[docs] def partial_read(self, filepath, width_percent, height_percent, x0_percent, y0_percent): # -> tuple(np.ndarray, dict)
"""
Returns an array of a part of an .tif image.
The arguments must be percentages, even the startingpoint is relative.
Starting Point is the top-left corner.
"""
x0,y0, x1,y1 = self._absolute_ROI_corners(filepath,
width_percent,
height_percent,
x0_percent,
y0_percent)
return tf.memmap(filepath, dtype= np.uint8)[y0:y1, x0:x1, :], {"ROI": (x0,y0,x1,y1)}
[docs] def preview(self, spim):
filepath = spim.metadata["filepath"]
x0,y0, x1,y1 = self._absolute_ROI_corners(filepath,
self.parameters["width_percent"].value,
self.parameters["height_percent"].value,
self.parameters["x0_percent"].value,
self.parameters["y0_percent"].value)
# read entire image
if self._preview_image_cache is None:
self._preview_image_cache = cv2.cvtColor(cv2.imread(filepath), cv2.COLOR_BGR2RGB)
image = self._preview_image_cache.copy()
start_point = x0,y0
end_point = x1,y1
image = (image*0.5).astype(np.uint8) # darken image
image[y0:y1, x0:x1] *= 2 # brighten ROI
# draw rectangle
image = cv2.rectangle(image, start_point, end_point, (0,255,0), thickness=1)
return image
[docs]class GreyscaleConverter(Converter):
"""Converts a color image to a greyscale image, by selecting one channel
or by converting it to another color space and then selecting one channel.
The supported options are given by the `conversion` parameter, which must be
one of the following strings
- `Hue`, `Saturation` or `Value` channel
- `Red`, `Blue` or `Green` color channel
- normal `Greyscale` conversion
It uses the `cv2.cvtColor` function.
Additionally the dark an bright parts can be switched using `invert=True`
"""
def __init__(self):
self.hsv_str_list = ["Hue", "Saturation", "Value"]
self.rgb_str_list = ["Red channel", "Blue channel", "Green channel"]
converter_options = ["Grey"] + self.hsv_str_list + self.rgb_str_list
pars = [EnumParameter("conversion",
converter_options[0],
converter_options),
BoolParameter("invert", False)]
super(GreyscaleConverter, self).__init__(self.convert, pars)
def convert(self, rgb_image, conversion, invert):
if conversion == "Grey":
out = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2GRAY).astype(np.uint8)
else:
if conversion in self.hsv_str_list:
code = cv2.COLOR_RGB2HSV
ch_nr = self.hsv_str_list.index(conversion)
out = cv2.cvtColor(rgb_image, code)
elif conversion in self.rgb_str_list:
ch_nr = self.rgb_str_list.index(conversion)
out = rgb_image
out = out[:, :, ch_nr].astype(np.uint8)
if invert:
return cv2.bitwise_not(out)
else:
return out
[docs]class GaussianPreprocess(Preprocessor):
"""Blur the image with a gaussian blur with kernel size given
by `ksize`. It uses the `cv2.filter2D` function
"""
def __init__(self, ksize):
pars = [NumericRangeParameter("kernelsize", ksize, 1, 47, step=2)]
super(GaussianPreprocess, self).__init__(self.filter_fn, pars)
def filter_fn(self, grey_image, kernelsize):
if kernelsize > 1:
kernel = np.ones((kernelsize, kernelsize), np.float32)
kernel /= kernelsize**2
return cv2.filter2D(grey_image, -1, kernel)
else:
return grey_image
[docs]class BinaryThreshold(Binarization):
"""Converts the image to a binary one, where the parts above
the given threshold are set to 255 and the parts below it to 0.
The sole parameter is the threshold value.
It uses the `cv2.threshold` function.
"""
def __init__(self, threshold):
pars = SpotlobParameterSet(
[NumericRangeParameter("threshold", threshold, 0, 255)])
super(BinaryThreshold, self).__init__(self.threshold_fn, pars)
def threshold_fn(self, grey_image, threshold):
_, im = cv2.threshold(
grey_image, threshold, 255, cv2.THRESH_BINARY)
return im
[docs]class OtsuThreshold(Binarization):
"""Performs a binarization based on Otsu's algorithm.
It uses the `cv2.threshold` function.
"""
def __init__(self):
super(OtsuThreshold, self).__init__(self.threshold_fn, [])
def threshold_fn(self, grey_image):
_, im = cv2.threshold(grey_image,
0,
255,
cv2.THRESH_OTSU)
return im
[docs]class PostprocessNothing(Postprocessor):
"""This process is used as a placeholder for a postprocessing step
and does not modify the image at all"""
def __init__(self):
super(PostprocessNothing, self).__init__(self.postprocess_fn, [])
def postprocess_fn(self, im):
return im
[docs]class ContourFinderSimple(FeatureFinder):
"""Finds contours, i.e. lists of points that enclose connected areas of
the same value. It is based on the `cv2.findContours` function
"""
def __init__(self):
super(ContourFinderSimple, self).__init__(self.finder_fn, [])
def finder_fn(self, bin_im):
cont_ret = cv2.findContours(bin_im,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# cont_ret is
# contours, hierarchy for opencv >4.0
# im, contours, hierarchy for opencv <=3.4
contours = cont_ret[-2]
return contours
[docs]class ContourFinder(FeatureFinder):
"""Finds contours, i.e. lists of points that enclose connected areas of
the same value. It is based on the `cv2.findContours` function.
It can distinguish between different levels of nested areas
Parameters
----------
mode : string
Select which kind of blobs should be found and the contour of
which should be returned. Select of the following
* all = all contours, both holes and non-holes
* inner = innermost blobs without holes in them
* outer = only outermost blobs
* holes = only holes, that are contained in other blobs
* non-holes = all blobs, that are not holes
"""
def __init__(self, mode):
mode_par = EnumParameter("mode", mode,
["outer",
"inner",
"all",
"holes",
"non-holes"])
super(ContourFinder, self).__init__(self.finder_fn, [mode_par])
def finder_fn(self, bin_im, mode):
if mode == "outer":
cont_ret = cv2.findContours(bin_im,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
return cont_ret[-2]
else:
cont_ret = cv2.findContours(bin_im,
cv2.RETR_CCOMP,
cv2.CHAIN_APPROX_SIMPLE)
# cont_ret is
# * contours, hierarchy for opencv >4.0
# * im, contours, hierarchy for opencv <=3.4
contours = cont_ret[-2]
hierarchy = cont_ret[-1][0]
if mode == 'all':
return contours
else:
holes = []
nonholes = []
inner = []
i = 0
nonholes_i = []
# first hierarchy is the nonholes
# parse through them
while i >= 0:
head = contours[i]
nonholes.append(head)
nonholes_i.append(i)
next_i, prev_i, child_i, parent_i = hierarchy[i]
if child_i < 0:
inner.append(head)
i = next_i # index of next contour on same hierarchy level
if mode == "holes":
# rest should be holes
# ie. all which are not nonholes
# list of holes is the contour list withouth the ones
# specified by nonholes index list
holes = np.delete(contours, nonholes_i, axis=0)
return holes
elif mode == "non-holes":
return nonholes
elif mode == "inner":
return inner
else:
raise NotImplementedError(
"Unsupported contour finder mode %s" % mode)
def draw_contours(color_image, contours, color=(0, 255, 0), thickness=1):
return cv2.drawContours(color_image, contours, -1, color, thickness)
def crop_to_contour(image, contours):
x, y, w, h = cv2.boundingRect(contours)
return image[y:y+h, x:x+w]