Source code for imars3d.backend.morph.crop

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""iMars3D: crop module."""
import logging
import numpy as np
import param
from scipy.ndimage import median_filter

logger = logging.getLogger(__name__)


[docs] class crop(param.ParameterizedFunction): """ Crop the image stack to provided limits or auto detected bound box. Parameters ---------- arrays: ndarray The image stack to crop. Can also be a 2D image. crop_limit: tuple The four limits for cropping. Default is (-1, -1, -1, -1), which will trigger the automatic bounds detection. border_pix: int the width of border region to estimate the background intensity, which helps to determine which case we are in. expand_ratio: float the ratio to expand the cropped region. rel_intensity_threshold_air_or_slit: float passing through keyword arguments to detect_bounds. rel_intensity_threshold_fov: float passing through keyword arguments to detect_bounds. rel_intensity_threshold_sample: float passing through keyword arguments to detect_bounds. Returns ------- The cropped image stack. """ # Notes # ----- # Case 1: slits in, i.e I_inner >> I_outer # o******************************************************* # | low | # | +++++++++++++++++top slit+++++++++++++++ | # | + + | # | + + | # | left slit FOV(high) right slit | # | + + | # | + + | # | +++++++++++++++++bottom slit+++++++++++++ | # | | # ******************************************************** # # Case 2: slits out, i.e I_inner < I_outer # o******************************************************* # | | # | ***************** | # | * object * air | # | * (low) * (high) | # | *********** | # ******************************************************** # # NOTE: fails early if the array dimension is incorrect arrays = param.Array( doc="The image stack to crop. Can also be a 2D image.", precedence=-1, # hide arrays from auto GUI, default=None, ) crop_limit = param.List( default=[-1, -1, -1, -1], doc="The four limits for cropping. Default is (-1, -1, -1, -1), which will trigger the automatic bounds detection.", precedence=1, # mandatory ) border_pix = param.Integer( default=10, precedence=0.5, # advanced option doc="the width of border region to estimate the background intensity, which helps to determine which case we are in.", ) expand_ratio = param.Number( default=0.1, precedence=0.4, doc="The ratio to expand the cropped region." # advanced option ) rel_intensity_threshold_air_or_slit = param.Number( default=0.05, precedence=0.3, doc="Passing through keyword arguments to detect_bounds." # advanced option ) rel_intensity_threshold_fov = param.Number( default=0.1, precedence=0.2, doc="Passing through keyword arguments to detect_bounds." # advanced option ) rel_intensity_threshold_sample = param.Number( default=0.95, precedence=0.1, doc="Passing through keyword arguments to detect_bounds." # advanced option ) def __call__(self, **params): """Call the function.""" logger.info("Executing Filter: Crop") # forced type+bounds check _ = self.instance(**params) # sanitize args params = param.ParamOverrides(self, params) cropped_array = self._crop( params.arrays, params.crop_limit, params.border_pix, params.expand_ratio, params.rel_intensity_threshold_air_or_slit, params.rel_intensity_threshold_fov, params.rel_intensity_threshold_sample, ) logger.info("FINISHED Executing Filter: Crop") return cropped_array def _crop( self, arrays, crop_limit, border_pix, expand_ratio, rel_intensity_threshold_air_or_slit, rel_intensity_threshold_fov, rel_intensity_threshold_sample, ) -> np.ndarray: """Private function to crop the image stack.""" if arrays.ndim not in (2, 3): raise ValueError("Only 2D and 3D arrays are supported.") # check if crop limits (bounding box) are provided if -1 in crop_limit: crop_limit = detect_bounds( arrays, border_pix, expand_ratio, rel_intensity_threshold_air_or_slit, rel_intensity_threshold_fov, rel_intensity_threshold_sample, ) # crop left, right, top, bottom = crop_limit if arrays.ndim == 2: return arrays[top:bottom, left:right] elif arrays.ndim == 3: # return a copy allowing the uncropped array can be garbage collected in the future return arrays[:, top:bottom, left:right].copy()
[docs] def detect_bounds( arrays: np.ndarray, border_pix: int = 10, expand_ratio: float = 0.1, rel_intensity_threshold_air_or_slit: float = 0.05, rel_intensity_threshold_fov: float = 0.1, rel_intensity_threshold_sample: float = 0.95, ) -> tuple: """ Auto detect bounds based on intensity thresholding. Parameters ---------- arrays: The image stack to crop. Can also be a 2D image. border_pix: the width of border region to estimate the background intensity expand_ratio: the ratio to expand the cropped region. rel_intensity_threshold_air_or_slit: the relative intensity threshold to determine whether the outter boarder is slit (case 1) or air (case 2). rel_intensity_threshold_fov: the relative intensity threshold used to determine pixels within to the field of view, only valid for case 1. rel_intensity_threshold_sample: the relative intensity threshold used to determine pixels within the sample region, only valid for case 2, and the value is relative to the intensity of the air region (outter region in case 2). Returns ------- The crop limits in (left, right, top, bottom) order. """ # generate representative image if arrays.ndim == 2: img = arrays elif arrays.ndim == 3: img = arrays.mean(axis=0) else: raise ValueError("Only 2D and 3D arrays are supported.") # denoise img = median_filter(img, 9).astype(float) # rescale img = (img - img.min()) / (img.max() - img.min()) # estimate background from four stripes near the border left = np.median(img[:, :border_pix]) right = np.median(img[:, -border_pix:]) top = np.median(img[:border_pix, :]) bottom = np.median(img[-border_pix:, :]) intensity_bg = np.median([left, right, top, bottom]) if intensity_bg < rel_intensity_threshold_air_or_slit: # Case 1: slits in, i.e I_inner >> I_outer # when the background intensity is around the bottom 5% of the # total dynamic range, we select the pixels with the top 90% intensity # to be the field of view. ys, xs = np.where(img > rel_intensity_threshold_fov) dx = dy = 0.0 # no expanding needed else: # Case 2: slits out, i.e I_inner < I_outer # when the estimated background intensity is high (> 5%), we assume # that the slits are either out or removed from image, therefore the # detection now is for the object within the FOV. # since object will absorb neutron, selecting the lower intensity # region helps us identify the rough bounding box. ys, xs = np.where(img < intensity_bg * rel_intensity_threshold_sample) # width = xs.max() - xs.min() height = ys.max() - ys.min() # dx = width * expand_ratio dy = height * expand_ratio # return the limits return ( int(max(xs.min() - dx, 0)), int(min(xs.max() + dx, img.shape[1])), int(max(ys.min() - dy, 0)), int(min(ys.max() + dy, img.shape[0])), )