# -*- coding: utf-8 -*-
"""Kerr Image Processing Module.
Created on Fri Apr 21 17:29:08 2017
Derivatives of ImageArray and ImageStack specific to processing Kerr images.
@author: phyrct
"""
__all__ = ["KerrArray", "KerrStack", "MaskStack"]
import os
import numpy as np
from ..Image import ImageArray, ImageStack, ImageFile
from ..tools import make_Data
from ..tools.decorators import class_modifier, image_file_adaptor
from . import kerrfuncs
try:
import pytesseract # pylint: disable=unused-import
_tesseractable = True
except ImportError:
pytesseract = None
_tesseractable = False
GRAY_RANGE = (0, 65535) # 2^16
IM_SIZE = (512, 672) # Standard Kerr image size
AN_IM_SIZE = (554, 672) # Kerr image with annotation not cropped
pattern_file = os.path.join(os.path.dirname(__file__), "kerr_patterns.txt")
[docs]@class_modifier(kerrfuncs)
class KerrArray(ImageArray):
"""A subclass for Kerr microscopy specific image functions."""
# useful_keys are metadata keys that we'd usually like to keep from a
# standard kerr output.
def __init__(self, *args, **kargs):
"""Initialise KerrArray as a subclasses ImageArray.
Extra keyword arguments accepted are given below.
Keyword Arguments:
reduce_metadata(bool):
if True reduce the metadata to useful bits and do some processing on it
asfloat(bool)
if True convert the image to float values between 0 and 1 (necessary
for some forms of processing)
crop_text(bool):
whether to crop the bottom text area from the image
ocr_metadata(bool):
whether to try to use optical character recognition to get the
metadata from the image (necessary for images taken pre 06/2016
and so far field from hysteresis images)
field_only(bool):
if ocr_metadata is true, get field only (bit faster)
"""
kerrdefaults = {
"ocr_metadata": False,
"field_only": False,
"reduce_metadata": True,
"asfloat": True,
"crop_text": True,
}
kerrdefaults.update(kargs)
super().__init__(*args, **kargs)
self._tesseractable = None
if kerrdefaults["reduce_metadata"]:
self.reduce_metadata()
if kerrdefaults["ocr_metadata"]:
self.ocr_metadata(field_only=kerrdefaults["field_only"])
if kerrdefaults["asfloat"]:
self.asfloat()
if kerrdefaults["crop_text"]:
self.crop_text()
@property
def tesseractable(self):
"""Do a test call to tesseract to see if it is there and cache the result."""
return _tesseractable
[docs] def save(self, filename=None, **kargs):
"""Stub method for a save function."""
raise NotImplementedError(f"Save is not implemented in {self.__class__}")
@class_modifier(kerrfuncs, adaptor=image_file_adaptor)
class KerrImageFile(ImageFile):
"""Subclass of ImageFile that keeps the data as a KerrArray so that extra functions are available."""
priority = 16
mime_type = ["image/png"]
pattern = ["*.png"]
def __init__(self, *args, **kargs):
"""Ensure that the image is a KerrImage."""
super().__init__(*args, **kargs)
self._image = self.image.view(KerrArray)
@ImageFile.image.getter
def image(self): # pylint disable=invalid-overridden-method
"""Access the image data."""
return self._image.view(KerrArray)
@ImageFile.image.setter
def image(self, v): # pylint: disable=function-redefined
"""Ensure stored image is always an ImageArray."""
filename = self.filename
v = KerrArray(v)
# ensure setting image goes into the same memory block if from stack
if (
hasattr(self, "_fromstack")
and self._fromstack
and self._image.shape == v.shape
and self._image.dtype == v.dtype
):
self._image[:] = v
self._image = self._image.view(KerrArray)
else:
self._image = KerrArray(v)
self.filename = filename
class KerrStackMixin:
"""A mixin for :py:class:`ImageStack` that adds some functionality particular to Kerr images.
Attributes:
fields(list):
list of applied fields in stack. This is the most important metadata
for things like hysteresis.
"""
_defaults = {"type": KerrImageFile}
@property
def fields(self):
"""Produce an array of field values from the metadata."""
if not hasattr(self, "_field"):
if "field" not in self.metadata:
self._field = np.arange(len(self))
else:
self._field = np.array(self.metadata["field"])
return self._field
def crop_text(self):
"""Crop the bottom text area from a standard Kermit image across the complete stack.
Returns:
(ImageArray):
cropped image
"""
images = self.shape[0]
if self.shape[1:3] == IM_SIZE:
return self
if self.shape[1:3] != AN_IM_SIZE:
raise ValueError(
f"Need a full sized Kerr image to crop. Current size is {self.shape}"
) # check it's a normal image
self._sizes = np.column_stack(
(np.ones(images, dtype=int) * IM_SIZE[0], np.ones(images, dtype=int) * IM_SIZE[1])
)
new_size = self.max_size + (images,)
self._resize_stack(new_size)
return self
def hysteresis(self, mask=None):
"""Make a hysteresis loop of the average intensity in the given images.
Keyword Argument:
mask(ndarray or list):
boolean array of same size as an image or imarray or list of
masks for each image. If True then don't include that area in
the intensity averaging.
Returns
-------
hyst(Data):
'Field', 'Intensity', 2 column array
"""
hyst = np.column_stack((self.fields, np.zeros(len(self))))
for i, im in enumerate(self):
if isinstance(im, ImageFile):
im = im.image
if isinstance(mask, np.ndarray) and mask.ndim == 2:
hyst[i, 1] = np.average(im[np.invert(mask.astype(bool))])
elif isinstance(mask, np.ndarray) and mask.ndim == 3:
hyst[i, 1] = np.average(im[np.invert(mask[i, :, :].astype(bool))])
elif isinstance(mask, (tuple, list)):
hyst[i, 1] = np.average(im[np.invert(mask[i])])
else:
hyst[i, 1] = np.average(im)
d = make_Data(hyst, setas="xy")
d.column_headers = ["Field", "Intensity"]
return d
def index_to_field(self, index_map):
"""Convert an image of index values into an image of field values."""
fieldvals = np.take(self.fields, index_map)
return ImageArray(fieldvals)
def denoise_thresh(self, denoise_weight=0.1, thresh=0.5, invert=False):
"""Apply denoise then threshold images.
Return:
(ndarray) MaskStack:
True for values greater than thresh, False otherwise
else return True for values between thresh and 1
"""
masks = self.clone
masks.each.denoise(weight=denoise_weight)
masks.each.threshold_minmax(threshmin=thresh, threshmax=np.max(masks.imarray))
masks = MaskStack(masks)
if invert:
masks.stack = ~masks.stack # pylint: disable=attribute-defined-outside-init
return masks
def find_threshold(self, testim=None, mask=None):
"""Try to find the threshold value at which the image switches.
Takes it as the median value of the testim. Masks values
where the difference is less than tolerance in case part of the image is
irrelevant.
"""
if testim is None:
testim = self[len(self) // 2]
elif isinstance(testim, (int, str)):
testim = self[testim]
elif isinstance(testim, np.ndarray) and testim.shape == self[len(self) // 2].shape:
pass
else:
raise ValueError("Cannot find testimage for thresholding.")
if mask is None:
med = testim.median()
else:
med = testim[~testim.mask]
return med
def stable_mask(self, tolerance=1e-2, comparison=(0, -1)):
"""Produce a mask of areas of the image that are changing little over the stack.
comparison is an optional tuple that gives the index of two images
to compare, otherwise first and last used. tolerance is the difference
tolerance
"""
first, last = comparison
mask = np.zeros(self[0].shape, dtype=bool)
mask[abs(self[last] - self[first]) < tolerance] = True
return mask
def HcMap(
self,
threshold=0.5,
correct_drift=False,
baseimage=0,
quiet=True,
saturation_end=True,
saturation_white=True,
extra_info=False,
):
"""Produce a map of the switching field at every pixel in the stack.
It needs the stack to start saturated one way and end saturated the other way.
Keyword Arguments:
threshold(float):
the threshold value for the intensity switching. This will need to
be tuned for each stack
correct_drift(bol):
whether to correct drift on the image stack before proceding
baseimage(int or ImageArray):
we use drift correction from the baseimage.
saturation_end(bool):
last image in stack is closest to saturation
saturation_white(bool):
bright pixels are saturated dark pixels are not yet switched
quiet: bool
choose wether to output status updates as print messages
extra_info(bool):
choose whether to return intermediate calculation steps as an extra dictionary
Returns:
(ImageArray): The map of field values for switching of each pixel in the stack
"""
ks = self.clone
if isinstance(baseimage, int):
baseimage = self[baseimage].clone
elif isinstance(baseimage, np.ndarray):
baseimage = baseimage.view(ImageArray)
if correct_drift:
ks.apply_all("correct_drift", ref=baseimage, quiet=quiet)
if not quiet:
print("drift correct done")
masks = self.denoise_thresh(denoise_weight=0.1, thresh=threshold, invert=not (saturation_white))
if not quiet:
print("thresholding done")
si, sp = masks.switch_index(saturation_end=saturation_end)
Hcmap = ks.index_to_field(si)
Hcmap[Hcmap == ks.fields[0]] = 0 # not switching does not give us a Hc value
if extra_info:
ei = {"switch_index": si, "switch_array": sp, "masks": masks}
return Hcmap, ei
return Hcmap
def average_Hcmap(self, weights=None, ignore_zeros=False):
"""Get an array of average pixel values for the stack.
Return average of pixel values in the stack.
Keyword Arguments:
ignore zeros(bool):
Weight zero values in an image as 0 in the averaging.
Returns:
average(ImageArray):
average values
"""
if ignore_zeros:
weights = self.clone
weights.imarray = weights.imarray.astype(bool).astype(int) # 1 if Hc isn't zero, zero otherwise
condition = np.sum(weights, axis=0) == 0 # stop zero division error
for m in range(self.shape[0]):
weights[m] = np.select([condition, np.logical_not(condition)], [np.ones_like(weights[m]), weights[m]])
# weights means we only account for non-zero values in average
average = np.average(self.imarray, axis=0, weights=weights)
return average.view(ImageArray)
class MaskStackMixin:
"""A Mixin for :py:class:`Stoner.Image.ImageStack` but made for stacks of boolean or binary images."""
def __init__(self, *args, **kargs):
"""Ensure the data is boolean."""
super().__init__(*args, **kargs)
self._stack = self._stack.astype(bool)
def switch_index(self, saturation_end=True, saturation_value=True):
"""Construct a map of switching points in a hysteresis stack.
Given a stack of boolean masks representing a hystersis loop find the stack index of the saturation
field for each pixel.
Take the final mask as all switched (or the first mask if saturation_end
is False). Work back through the masks taking the first time a pixel
switches as its coercive field (ie the last time it switches before
reaching saturation).
Elements that start switched at the lowest measured field or never
switch are given a zero index.
At the moment it's set up to expect masks to be false when the sample is saturated
at a high field
Keyword Arguments:
saturation_end(bool):
True if the last image is closest to the fully saturated state.
False if you want the first image
saturation_value(bool):
if True then a pixel value True means that switching has occurred
(ie magnetic saturation would be all True)
Returns:
switch_ind: MxN ndarray of int
index that each pixel switches at
switch_progession: MxNx(P-1) ndarray of bool
stack of masks showing when each pixel saturates
"""
ms = self.clone
if not saturation_end:
ms = ms.reverse()
# arr1 = ms[0].astype(float) #find out whether True is at begin or end
# arr2 = ms[-1].astype(float)
# if np.average(arr1)>np.average(arr2): #OK so it's bright at the start
if not saturation_value:
self.imarray = np.invert(ms.imarray) # Now it's bright (True) at end
switch_ind = np.zeros(ms[0].shape, dtype=int)
switch_prog = self.clone
switch_prog.imarray = np.zeros(self.shape, dtype=bool)
del switch_prog[-1]
for m in reversed(range(len(ms) - 1)): # go from saturation backwards
already_done = np.copy(switch_ind).astype(dtype=bool) # only change switch_ind if it hasn't already
condition = np.logical_and(not ms[m], ms[m + 1])
condition = np.logical_and(condition, np.invert(already_done))
condition = [condition, np.logical_not(condition)]
choice = [np.ones(switch_ind.shape) * m, switch_ind] # index or leave as is
switch_ind = np.select(condition, choice)
switch_prog[m] = already_done
if not saturation_end:
switch_ind = -switch_ind + len(self) - 1 # should check this!
switch_prog.reverse()
switch_ind = ImageArray(switch_ind.astype(int))
return switch_ind, switch_prog
[docs]class KerrStack(KerrStackMixin, ImageStack):
"""Represent a stack of Kerr images."""
[docs]class MaskStack(MaskStackMixin, KerrStackMixin, ImageStack):
"""Represent a set of masks for Kerr images."""