text: import inspect
  import logging
  import os
  import pathlib
  import shutil
  
  import cv2
  import numpy as np
  from scipy import ndimage
  
  
  def normalize(mask, dtype=np.uint8):
      return (255 * mask / np.amax(mask)).astype(dtype)
  
  
  def get_bounding_box(img):
      """Get bounding box coordinate information."""
      rows = np.any(img, axis=1)
      cols = np.any(img, axis=0)
      rmin, rmax = np.where(rows)[0][[0, -1]]
      cmin, cmax = np.where(cols)[0][[0, -1]]
      # due to python indexing, need to add 1 to max
      # else accessing will be 1px in the box, not out
      rmax += 1
      cmax += 1
      return [rmin, rmax, cmin, cmax]
  
  
  def cropping_center(x, crop_shape, batch=False):
      """Crop an input image at the centre.
  
      Args:
          x: input array
          crop_shape: dimensions of cropped array
  
      Returns:
          x: cropped array
  
      """
      orig_shape = x.shape
      if not batch:
          h0 = int((orig_shape[0] - crop_shape[0]) * 0.5)
          w0 = int((orig_shape[1] - crop_shape[1]) * 0.5)
          x = x[h0: h0 + crop_shape[0], w0: w0 + crop_shape[1]]
      else:
          h0 = int((orig_shape[1] - crop_shape[0]) * 0.5)
          w0 = int((orig_shape[2] - crop_shape[1]) * 0.5)
          x = x[:, h0: h0 + crop_shape[0], w0: w0 + crop_shape[1]]
      return x
  
  
  def rm_n_mkdir(dir_path):
      """Remove and make directory."""
      if os.path.isdir(dir_path):
          shutil.rmtree(dir_path)
      os.makedirs(dir_path)
  
  
  def rmdir(dir_path):
      if os.path.isdir(dir_path):
          shutil.rmtree(dir_path)
      return
  
  
  def mkdir(dir_path):
      """Make directory."""
      if not os.path.isdir(dir_path):
          os.makedirs(dir_path)
  
  
  def recur_find_ext(root_dir, ext):
      """
      recursively find all files in directories end with the `ext`
      such as `ext='.png'`
  
      return list is alrd sorted
      """
      if not isinstance(ext, list):
          ext = [ext]
  
      file_path_list = []
      for cur_path, dir_list, file_list in os.walk(root_dir):
          for file_name in file_list:
              file_ext = pathlib.Path(file_name).suffix
              if file_ext in ext:
                  full_path = os.path.join(cur_path, file_name)
                  file_path_list.append(full_path)
      file_path_list.sort()
      return file_path_list
  
  
  def get_inst_centroid(inst_map):
      """Get instance centroids given an input instance map.
  
      Args:
          inst_map: input instance map
  
      Returns:
          array of centroids
  
      """
      inst_centroid_list = []
      inst_id_list = list(np.unique(inst_map))
      for inst_id in inst_id_list[1:]:  # avoid 0 i.e background
          mask = np.array(inst_map == inst_id, np.uint8)
          inst_moment = cv2.moments(mask)
          inst_centroid = [
              (inst_moment["m10"] / inst_moment["m00"]),
              (inst_moment["m01"] / inst_moment["m00"]),
          ]
          inst_centroid_list.append(inst_centroid)
      return np.array(inst_centroid_list)
  
  
  def center_pad_to_shape(img, size, cval=255):
      """Pad input image."""
      # rounding down, add 1
      pad_h = size[0] - img.shape[0]
      pad_w = size[1] - img.shape[1]
      pad_h = (pad_h // 2, pad_h - pad_h // 2)
      pad_w = (pad_w // 2, pad_w - pad_w // 2)
      if len(img.shape) == 2:
          pad_shape = (pad_h, pad_w)
      else:
          pad_shape = (pad_h, pad_w, (0, 0))
      img = np.pad(img, pad_shape, "constant", constant_values=cval)
      return img
  
  
  def color_deconvolution(rgb, stain_mat):
      """Apply colour deconvolution."""
      log255 = np.log(255)  # to base 10, not base e
      rgb_float = rgb.astype(np.float64)
      log_rgb = -((255.0 * np.log((rgb_float + 1) / 255.0)) / log255)
      output = np.exp(-(log_rgb @ stain_mat - 255.0) * log255 / 255.0)
      output[output > 255] = 255
      output = np.floor(output + 0.5).astype("uint8")
      return output
  
  
  def log_debug(msg):
      (
          frame, filename, line_number,
          function_name, lines, index
      ) = inspect.getouterframes(
          inspect.currentframe()
      )[1]
      line = lines[0]
      indentation_level = line.find(line.lstrip())
      logging.debug("{i} {m}".format(i="." * indentation_level, m=msg))
  
  
  def log_info(msg):
      (
          frame, filename, line_number,
          function_name, lines, index
      ) = inspect.getouterframes(
          inspect.currentframe()
      )[1]
      line = lines[0]
      indentation_level = line.find(line.lstrip())
      logging.info("{i} {m}".format(i="." * indentation_level, m=msg))
  
  
  def remove_small_objects(pred, min_size=64, connectivity=1):
      """Remove connected components smaller than the specified size.
  
      This function is taken from skimage.morphology.remove_small_objects,
      but the warning is removed when a single label is provided.
  
      Args:
          pred: input labelled array
          min_size: minimum size of instance in output array
          connectivity: The connectivity defining the neighborhood of a pixel.
  
      Returns:
          out: output array with instances removed under min_size
  
      """
      out = pred
  
      if min_size == 0:  # shortcut for efficiency
          return out
  
      if out.dtype == bool:
          selem = ndimage.generate_binary_structure(pred.ndim, connectivity)
          ccs = np.zeros_like(pred, dtype=np.int32)
          ndimage.label(pred, selem, output=ccs)
      else:
          ccs = out
  
      try:
          component_sizes = np.bincount(ccs.ravel())
      except ValueError:
          raise ValueError(
              "Negative value labels are not supported. Try "
              "relabeling the input with `scipy.ndimage.label` or "
              "`skimage.morphology.label`."
          )
  
      too_small = component_sizes < min_size
      too_small_mask = too_small[ccs]
      out[too_small_mask] = 0
  
      return out
  