Source code for Stoner.Image.kerr

# -*- 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 import make_Data
from import class_modifier, image_file_adaptor
from . import kerrfuncs

    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
@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] =[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 occured (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 =, 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."""