"""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