Source code for lavaflow.processing

"""Functions for image processing.
"""

import cv2
import lensfunpy
import numpy as np


# -----------------------------------------------------------------------------

# Size

[docs]class Resizer(object): """Class for memory efficient image resizing. """ def __init__(self, res): """ Args: res (tuple): target resolution ``(w, h)`` """ self.res = res
[docs] def __call__(self, img): """Resize image. Args: img (np.ndarray): image Returns: img (np.ndarray): image resized """ if img.shape[1] != self.res[0] or img.shape[0] != self.res[1]: return cv2.resize(img, self.res, 0, 0, cv2.INTER_AREA) else: return img
# ----------------------------------------------------------------------------- # Colors
[docs]class ColorConverter(object): """Class for memory efficienct image processing. """ def __init__(self, code, astype=None): """ Args: code (np.ndarray): color conversion code astype (type): output type conversion (optional) """ self.code = code self.astype = astype
[docs] def __call__(self, img): """Convert an image from one color space to another. Args: img (np.ndarray): image Returns: img (np.ndarray): image with color conversion applied """ # TODO: check if output should always be converted to np.unit8 tmp = cv2.cvtColor(img, self.code) if self.astype is not None: return tmp.astype(self.astype) else: return tmp
# ----------------------------------------------------------------------------- # Adjustments
[docs]class LUTAdjuster(object): """Class for memory efficienct image processing. """ def __init__(self, LUT): """ Args: LUT (np.ndarray): look up table """ self.LUT = LUT self.identity = LUT is None or np.array_equal(LUT, np.arange(0, 256).astype(np.uint8))
[docs] def __call__(self, img): """Apply look up table to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with look up table applied """ if self.identity: return img else: return cv2.LUT(img, self.LUT)
[docs]class GammaAdjuster(LUTAdjuster): """Class for memory efficienct image processing. """ def __init__(self, gamma=1, gain=1): """ Args: gamma (float): gamma value, range ``[0, inf)`` gain (float): gain value, range ``[0, inf)`` """ if gamma != 1 or gain != 1: super().__init__((((np.arange(0, 256) / 255.0) ** (1 / gamma)) * 255.0 * gain).clip(0, 255).astype(np.uint8)) else: super().__init__(None)
[docs]class LinearAdjuster(LUTAdjuster): """Class for memory efficienct image processing. """ def __init__(self, gain=1, bias=0): """ Args: gain (float): gain value, range ``(-inf, inf)`` bias (float): bias value, range ``[-255, 255]`` """ if gain != 1 or bias != 0: super().__init__(np.array([gain * i + bias for i in np.arange(0, 256)]).clip(0, 255).astype(np.uint8)) else: super().__init__(None)
[docs]class BrightnessAndContrastAdjuster(LinearAdjuster): """Class for memory efficienct image processing. """ def __init__(self, brightness=0, contrast=0, center=128): """ Args: brightness (float): brightness, range ``[-1, 1]`` contrast (float): contrast, range ``[-8, 8]`` center (float): center of contrast adjustment, range [0, 255] """ gain = 2 ** contrast bias = brightness * 255 + center * (1 - gain) super().__init__(gain, bias)
[docs]class CurveAdjuster(object): """Class for memory efficienct image processing. """ def __init__(self, slope=0, shift=0): """ Args: slope (float): slope factor, range ``[-1, 1]`` shift (float): shift factor, range ``[-1, 1]`` """ self.d = np.sign(shift) * np.abs(shift) ** 10 if np.abs(slope) == 1: self.r = np.sign(slope) * (1) else: self.r = np.sign(slope) * (1 - np.exp(1 - 1 / (1 - np.abs(slope)))) self.r = self.r * (0.5 - np.abs(self.d) / 2) / 0.5 self.p = (0.5 + self.d / 2) - self.r / 2 self.q = (0.5 + self.d / 2) + self.r / 2 self.z = (1 - shift) / 2 * 255 if self.p != 0 and self.q != 0: self.a = -np.log(1 / np.float64(self.p) - 1) self.b = -np.log(1 / np.float64(self.q) - 1)
[docs] def __call__(self, img): """Apply curve ajustment to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with curve adjustment applied """ if self.r == 0: return img if self.d == 1: return np.tile(np.uint8(255), img.shape) if self.d == -1: return np.tile(np.uint8(0), img.shape) if self.r == 1 or (self.r != -1 and (self.q == 0 or self.q == 1)): new = img.copy() new[(new <= self.z)] = 0 new[(new > self.z)] = 255 return new if self.r == -1 or (self.r != 1 and (self.p == 0 or self.p == 1)): new = img.copy() new[(new > 0) & (new < 255)] = 255 - self.z return new if self.r > 0: return (((1 / (1 + np.exp(-(self.a + (self.b - self.a) * img.astype(np.float64) / 255))) - (0.5 + self.d / 2)) + (self.q - self.p) / 2) / (self.q - self.p) * 255).astype(np.uint8) if self.r < 0: return (- (self.a + np.log(1 / (self.p + (self.q - self.p) * img.astype(np.float64) / 255) - 1)) / (self.b - self.a) * 255).astype(np.uint8)
# ----------------------------------------------------------------------------- # Sharpening
[docs]class KernelSharpener(object): """Class for memory efficienct image processing. """ def __init__(self, s=0): """ Args: s (float): sharpen parameter, range ``[0, inf)`` """ r = -s / 4 self.kernel = np.array([[0, r, 0], [r, s + 1, r], [0, r, 0]]) self.identity = s == 1
[docs] def __call__(self, img): """Apply curve ajustment to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with sharpening applied """ if self.identity: return img else: return cv2.filter2D(img, -1, self.kernel)
[docs]class UnmaskSharpener(object): """Class for memory efficienct image processing. """ def __init__(self, s=0, radius=5, spread=5): """ Args: s (float): sharpen parameter, range ``[0, inf)`` radius (float): kernel radius, range ``[1, inf)`` spread (float): kernel spread, range ``[1, inf)`` """ self.s = s self.radius = radius self.spread = spread self.identity = self.s == 0 or self.radius == 1
[docs] def __call__(self, img): """Apply curve ajustment to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with sharpening applied """ if self.identity: return img else: blr = cv2.GaussianBlur(img, (self.radius, self.radius), self.spread, None, self.spread) return (float(self.s + 1) * img - float(self.s) * blr).clip(0, 255).astype(np.uint8)
# ----------------------------------------------------------------------------- # Denoising
[docs]class NonLocalMeansDenoiser(object): """Class for memory efficienct image processing. """ def __init__(self, l=0, c=0, template_radius=7, search_radius=21): """ Args: l (float): lightness denoising parameter, range ``[0, inf)`` c (float): color denoising parameter, range ``[0, inf)`` template_radius (float): kernel radius, range ``[1, inf)`` search_radius (float): kernel radius, range ``[1, inf)`` """ self.l = l self.c = c self.template_radius = template_radius self.search_radius = search_radius self.identity = (self.l == 0 and self.c == 0) or self.template_radius == 1 or self.search_radius == 1
[docs] def __call__(self, img): """Apply curve ajustment to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with denoising applied """ if self.identity: return img else: return cv2.fastNlMeansDenoisingColored(img, None, self.l, self.c, self.template_radius, self.search_radius)
# ----------------------------------------------------------------------------- # Lens profile corrections
[docs]class LensCorrector(object): """Contruct class for lens correction based on the following - metadata extracted using https://exiftool.org/ - lens profiles from https://wilson.bronger.org/lensfun_coverage.html """ def __init__(self, maker, model, lens, focal_length, aperture, distance): """ Args: maker (str): camera maker, e.g. ``Canon`` model (str): camera model,e.g. ``Canon 90D`` lens (str): lens model, e.g. ``EF-S 18-135mm f/3.5-5.6 IS USM`` focal_length (double): focal length based on camera settings aperture (double): aperture based on camera settings distance (double): actual focal distance based on camera settings (m) """ self.camera = None self.lens = None self.focal_length = focal_length self.aperture = aperture self.distance = distance db = lensfunpy.Database() cameras = db.find_cameras(maker, model) if cameras: self.camera = cameras[0] lenses = db.find_lenses(self.camera, maker, lens) if lenses: self.lens = lenses[0]
[docs] def __call__(self, img): """Apply lens correction to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with denoising applied """ if self.camera and self.lens: mod = lensfunpy.Modifier(self.lens, self.camera.crop_factor, img.shape[1], img.shape[0]) mod.initialize(self.focal_length, self.aperture, self.distance) undist_coords = mod.apply_geometry_distortion() return cv2.remap(img, undist_coords, None, cv2.INTER_LANCZOS4) else: return img # TODO: could raise an exception instead
# ----------------------------------------------------------------------------- # Perspective transformations
[docs]class PerspectiveTransformer(object): """Class for perspective transformation using OpenCV. See ``cv2.getPerspectiveTransform`` and ``cv2.warpPerspective``. For example, :: perspective_transformer = PerspectiveTransformer( [[7, 37], [1517, 2], [1503, 952], [26, 919]], [[0, 0], [1920, 0], [1920, 1080], [0, 1080]] ) """ def __init__(self, src, dst): """ Args: src (list|np.array): list or array of source quadrangle points, shaped ``(N, 2)`` dst (list|np.array): list or array of destination quadrangle points, shaped ``(N, 2)`` """ self.src = np.array(src) self.dst = np.array(dst) self.xmin, self.ymin = self.dst.min(axis=0) self.xmax, self.ymax = self.dst.max(axis=0) self.M = cv2.getPerspectiveTransform(self.src.astype(np.float32), self.dst.astype(np.float32))
[docs] def __call__(self, img): """Apply perspective transformation to an image. Args: img (np.ndarray): image Returns: img (np.ndarray): image with perspective transformation applied """ img = cv2.warpPerspective(img, self.M, (img.shape[1], img.shape[0])) return img[self.ymin:self.ymax, self.xmin:self.xmax]