Source code for farabio.data.imgops

import os
import random
import skimage
import PIL
import numpy as np
from skimage import io
from PIL import Image
from skimage.morphology import convex_hull_image
import matplotlib.pyplot as plt


PIL.Image.MAX_IMAGE_PIXELS = 933120000

UNITS_MAPPING = [
    (1 << 50, ' PB'),
    (1 << 40, ' TB'),
    (1 << 30, ' GB'),
    (1 << 20, ' MB'),
    (1 << 10, ' KB'),
    (1, (' byte', ' bytes')),
]


def pretty_size(bytes, units=UNITS_MAPPING):
    for factor, suffix in units:
        if bytes >= factor:
            break
    amount = int(bytes / factor)

    if isinstance(suffix, tuple):
        singular, multiple = suffix
        if amount == 1:
            suffix = singular
        else:
            suffix = multiple
    return str(amount) + suffix


[docs]class ImgOps: """Creates instance of ImgOps class Attributes ---------- imgpath : str image path of interest fsize : str file size in human readable format img : numpy.array image h : int height w : int width ch : int channels pmax : int | float max pixel value pmin : int | float min pixel value img_r : numpy.array red channel image img_g : numpy.array green channel image img_b : numpy.array blue channel image Methods ------- get_date(self) Returns created date and hour info print_imginfo(self) Prints image information slice_img(img, slices, orien) Slices image into pieces pad_img(self, droi, simg=None) Pads image into predefined ROI size approx_bcg(self, channel='blue') Approximates background image blend_img(self, ref_, overlap=0.2, ratio=0.5) Blends two images taking the overage of overlap area mask_img(self, bw) Masks image profile_img(self, pt1, pt2) Plots image x/y profile between two coordinates """ def __init__(self, imgpath): self.imgpath = imgpath self.fsize = pretty_size(os.path.getsize(self.imgpath)) self.img = skimage.io.imread(self.imgpath, plugin='pil') self.h = self.img.shape[0] self.w = self.img.shape[1] self.ch = 1 if len(self.img.shape) > 2: self.ch = self.img.shape[2] self.size = (self.h, self.w, self.ch) self.pmax = self.img.max() self.pmin = self.img.min() self.gray = self.img if self.ch == 3: self.img_r = self.img[:, :, 0] self.img_g = self.img[:, :, 1] self.img_b = self.img[:, :, 2] self.gray = 255*skimage.color.rgb2gray(self.img)
[docs] def get_date(self): """Get created date and hour info Returns ------- str Date and Hour, ex: 200820_191645 Examples ---------- >>> ImgOps(img_path).get_date() """ img_exif = Image.open(self.imgpath)._getexif() if img_exif: img_timestamp = img_exif[36868] img_day = img_timestamp.split()[0][2:].replace(':', '') img_hour = img_timestamp.split()[1].replace(':', '') img_date = '_'.join([img_day, img_hour]) self.img_date = img_date else: img_date = "" return img_date
[docs] def print_imginfo(self): """Prints information pm image Returns ------- info Path, Shape, Intensity range and file size Examples ---------- >>> ImgOps(img_path).print_imginfo() """ print("Path:", self.imgpath) print("Shape:", self.img.shape) print("Intensity Range: [", self.pmin, self.pmax, "]") print("File size:", self.fsize)
[docs] def slice_img(self, slices, orien): """Slices image into pieces Parameters ---------- img : numpy.array image slices : int number of slices orien : str orientation to cut. 'x': vertical, 'y': horizontal Returns ------- img_slices : list of numpy.array image slices img_slices_info : list of tuples represents sliced image dimensions Examples ---------- >>> ImgOps(img_path).slice_img(slices=2,orien='x') """ if len(self.img.shape) == 2: (h, w) = np.shape(self.img) if len(self.img.shape) == 3: (h, w, ch) = np.shape(self.img) img_slices = [] if orien == 'y': for i in range(slices): slice_start = 1 if i == 0 else (i*h) // slices slice_end = ((i+1)*h) // slices if ch == 1: img_slice = self.img[slice_start:slice_end, :] if ch == 3: img_slice = self.img[slice_start:slice_end, :, :] img_slices.append(img_slice) if orien == 'x': for i in range(slices): slice_start = 1 if i == 0 else (i*w) // slices slice_end = ((i+1)*w) // slices img_slice = self.img[:, slice_start:slice_end, :] if ch == 3 else self.img[:, slice_start:slice_end] img_slices.append(img_slice) img_slices_info = [np.shape(img_slices[i]) for i in range(len(img_slices))] return img_slices, img_slices_info
[docs] def pad_img(self, droi, simg=None): """Pads image into predefined ROI size Parameters ---------- img : numpy.array image droi : tuple desired ROI shape. Ex: (1024, 1024) for gray, (1024, 1024, 3) for RGB simg : tuple optional. custom definitions for object of interest Returns --------- img_pad : numpy.array padded image Examples ---------- >>> ImgOps(img_path).pad_img((1024, 1024, 3)) """ assert type(droi) == tuple, "param at index 1 must be tuple" if simg is None: simg = np.shape(self.img) dh = droi[0] - simg[0] dw = droi[1] - simg[1] pad_h = dh // 2 pad_w = dw // 2 if len(simg) > 2: droi = (*droi, simg[-1]) img_pad = np.zeros(droi, dtype=self.img.dtype) if len(self.img.shape) == 2: img_pad = np.zeros(droi, dtype=self.img.dtype) img_pad[pad_h:(pad_h+simg[0]), pad_w:(pad_w+simg[-1])] = self.img if len(self.img.shape) == 3: img_pad[pad_h:(pad_h+simg[0]), pad_w:(pad_w+simg[1]), 0] = self.img[:, :, 0] img_pad[pad_h:(pad_h+simg[0]), pad_w:(pad_w+simg[1]), 1] = self.img[:, :, 1] img_pad[pad_h:(pad_h+simg[0]), pad_w:(pad_w+simg[1]), 2] = self.img[:, :, 2] return img_pad
[docs] def approx_bcg(self, channel='blue'): """Approximates background image Parameters ---------- channel : str colour channel Returns ------- img_pad : numpy.array approximate background image Examples ---------- >>> ImgOps(img_path).approx_bcg(channel='blue') """ min_r, min_g, min_b = [np.min(self.img[:, :, i]) for i in range(0, 3)] max_r, max_g, max_b = [np.max(self.img[:, :, i]) for i in range(0, 3)] self.img_bcg = np.zeros((self.h, self.w), dtype='uint8') if channel == 'red': self.img_bcg = np.zeros((self.h, self.w), dtype='uint8') for x in range(min_r, max_r): img_hull = convex_hull_image(self.img_r > x) self.img_bcg[img_hull] = x if channel == 'green': self.img_bcg = np.zeros((self.h, self.w), dtype='uint8') for y in range(min_g, max_g): img_hull = convex_hull_image(self.img_r > y) self.img_bcg[img_hull] = y if channel == 'blue': self.img_bcg = np.zeros((self.h, self.w), dtype='uint8') for z in range(min_b, max_b): img_hull = convex_hull_image(self.img_b > z) self.img_bcg[img_hull] = z return self.img_bcg
[docs] def blend_img(self, ref_, overlap=0.2, ratio=0.5): """Blends two images taking the average of overlap area Parameters ---------- ref_ : numpy.array image to blend with overlap : float opt. ratio of overlap area ratio : float opt. blending ratio Returns ------- img_blend : numpy.array blended image Examples ---------- >>> img_ref = ImgOps('./imgtoblend.png').img >>> ImgOps(img_path).blend_img(img_ref, overlap=0.3, ratio=0.5) """ assert self.img.shape == ref_.shape, "Shape must be same with reference image" h, w, ch = self.h, self.w, self.ch img_blend = np.zeros((h, round((2 - overlap)*w), ch)) img_blend[:, 0:round((1-overlap)*w), :] = self.img[:, 0:round((1-overlap)*w), :] img_blend[:, w:round((2-overlap)*w), :] = ref_[:, round(overlap*w):w, :] img_blend[:, round((1-overlap)*w):w, :] = ratio * self.img[:, round( (1-overlap)*w):w, :] + (1-ratio) * ref_[:, 0:round(overlap*w), :] img_blend = 255 * img_blend / np.amax(img_blend) img_blend = img_blend.astype(np.int) return img_blend
[docs] @staticmethod def blend_imgs(img1, img2, overlap=0.2, ratio=0.5): """ This function blends two images taking the overage of overlap area Parameters ---------- img1 : numpy.array First image to blend img2 : numpy.array Second image to blend overlap : float optional. ratio of overlap area ratio : float optional. blending ratio Returns ------- img_blend : numpy.array blended image Examples ---------- >>> ImgOps().blend_imgs("img1.jpg","img2.jpg",ratio=0.3) """ h, w, ch = img1.shape img_blend = np.zeros((h, round((2 - overlap)*w), ch)) img_blend[:, 0:round((1-overlap)*w), :] = img1[:, 0:round((1-overlap)*w), :] img_blend[:, w:round((2-overlap)*w), :] = img2[:, round(overlap*w):w, :] img_blend[:, round((1-overlap)*w):w, :] = ratio * img1[:, round( (1-overlap)*w):w, :] + (1-ratio) * img2[:, 0:round(overlap*w), :] img_blend = 255 * img_blend / np.amax(img_blend) img_blend = img_blend.astype(np.int) return img_blend
[docs] def mask_img(self, bw): """Masks image Parameters ---------- bw : numpy.array Binary mask Returns ------- img_ov : numpy.array Overlay image Examples ---------- >>> img_mask = ImgOps('./imgmask.png').img >>> ImgOps(img_path).mask_img(img_mask) """ img_ov = self.img img_ov[:, :, 0][bw == 0] = 0 img_ov[:, :, 1][bw == 0] = 0 img_ov[:, :, 2][bw == 0] = 0 return img_ov
[docs] @staticmethod def random_flip(img, y_random=False, x_random=False, return_param=False, copy=False): """Randomly flip an image in vertical or horizontal direction. Args: img (~numpy.ndarray): An array that gets flipped. This is in CHW format. y_random (bool): Randomly flip in vertical direction. x_random (bool): Randomly flip in horizontal direction. return_param (bool): Returns information of flip. copy (bool): If False, a view of :obj:`img` will be returned. Returns: ~numpy.ndarray or (~numpy.ndarray, dict): If :obj:`return_param = False`, returns an array :obj:`out_img` that is the result of flipping. If :obj:`return_param = True`, returns a tuple whose elements are :obj:`out_img, param`. :obj:`param` is a dictionary of intermediate parameters whose contents are listed below with key, value-type and the description of the value. * **y_flip** (*bool*): Whether the image was flipped in the\ vertical direction or not. * **x_flip** (*bool*): Whether the image was flipped in the\ horizontal direction or not. """ y_flip, x_flip = False, False if y_random: y_flip = random.choice([True, False]) if x_random: x_flip = random.choice([True, False]) if y_flip: img = img[:, ::-1, :] if x_flip: img = img[:, :, ::-1] if copy: img = img.copy() if return_param: return img, {'y_flip': y_flip, 'x_flip': x_flip} else: return img
[docs] @staticmethod def read_image(path, dtype=np.float32, color=True): """Read an image from a file. This function reads an image from given file. The image is CHW format and the range of its value is :math:`[0, 255]`. If :obj:`color = True`, the order of the channels is RGB. Args: path (str): A path of image file. dtype: The type of array. The default value is :obj:`~numpy.float32`. color (bool): The option determines # channels. RGB if :obj:`True`, grayscale for :obj:`False` Returns: ~numpy.ndarray: An image. """ f = Image.open(path) try: if color: img = f.convert('RGB') else: img = f.convert('P') img = np.asarray(img, dtype=dtype) finally: if hasattr(f, 'close'): f.close() if img.ndim == 2: # reshape (H, W) -> (1, H, W) return img[np.newaxis] else: # transpose (H, W, C) -> (C, H, W) return img.transpose((2, 0, 1))
[docs] def profile_img(self, pt1, pt2): """Plots image profile (x and y) Parameters ---------- pt1 : tuple coordinates of first point (row1, col1) pt2 : tuple coordinates of second point (row2, col2) Returns ------- fig : matplotlib figure projection of histograms Examples -------- >>> ImgOps(img_path).profile_img((50,50), (100,100)) """ # horizontal cols = np.arange(pt1[-1], pt2[-1]) hor_proj = np.sum(self.gray[:, pt1[-1]:pt2[-1]], axis=0) # vertical rows = np.arange(pt1[0], pt2[0]) vert_proj = np.sum(self.gray[pt1[0]:pt2[0], :], axis=1) fig, axs = plt.subplots(2, 1) axs[0].plot(cols, hor_proj) axs[0].title.set_text('Horizontal Profile') axs[1].plot(rows, vert_proj) axs[1].title.set_text('Vertical Profile') plt.tight_layout() plt.show() return fig