from enum import IntEnum
from typing import Tuple, Union
import cv2
import numexpr as ne
import numpy as np
class ImageProcessor:
"""
Generic image processor for numpy images
arguments
img np.ndarray HW (2 ndim)
HWC (3 ndim)
NHWC (4 ndim)
"""
def __init__(self, img : np.ndarray, copy=False):
if copy:
img = img.copy()
ndim = img.ndim
if ndim not in [2,3,4]:
raise ValueError(f'img.ndim must be 2,3,4, not {ndim}.')
# Make internal image as NHWC
if ndim == 2:
N, (H,W), C = 0, img.shape, 0
img = img[None,:,:,None]
elif ndim == 3:
N, (H,W,C) = 0, img.shape
img = img[None,...]
else:
N,H,W,C = img.shape
self._img : np.ndarray = img
def copy(self) -> 'ImageProcessor':
"""
"""
ip = ImageProcessor.__new__(ImageProcessor)
ip._img = self._img.copy()
return ip
def get_dims(self) -> Tuple[int,int,int,int]:
"""
returns dimensions of current working image
returns N,H,W,C (ints) , each >= 1
"""
return self._img.shape
def get_dtype(self):
return self._img.dtype
def gamma(self, red : float, green : float, blue : float, mask=None) -> 'ImageProcessor':
dtype = self.get_dtype()
self.to_ufloat32()
img = orig_img = self._img
img = np.power(img, np.array([1.0 / blue, 1.0 / green, 1.0 / red], np.float32) )
np.clip(img, 0, 1.0, out=img)
if mask is not None:
mask = self._check_normalize_mask(mask)
img = ne.evaluate('orig_img*(1-mask) + img*mask')
self._img = img
self.to_dtype(dtype)
return self
def apply(self, func, mask=None) -> 'ImageProcessor':
"""
apply your own function on internal image
image has NHWC format. Do not change format, but dims can be changed.
func callable (img) -> img
example:
.apply( lambda img: img-[102,127,63] )
"""
img = orig_img = self._img
img = func(img).astype(orig_img.dtype)
if img.ndim != 4:
raise Exception('func used in ImageProcessor.apply changed format of image')
if mask is not None:
mask = self._check_normalize_mask(mask)
img = ne.evaluate('orig_img*(1-mask) + img*mask').astype(orig_img.dtype)
self._img = img
return self
def fit_in (self, TW = None, TH = None, pad_to_target : bool = False, allow_upscale : bool = False, interpolation : 'ImageProcessor.Interpolation' = None) -> float:
"""
fit image in w,h keeping aspect ratio
TW,TH int/None target width,height
pad_to_target bool pad remain area with zeros
allow_upscale bool if image smaller than TW,TH it will be upscaled
interpolation ImageProcessor.Interpolation. value
returns scale float value
"""
#if interpolation is None:
# interpolation = ImageProcessor.Interpolation.LINEAR
img = self._img
N,H,W,C = img.shape
if TW is not None and TH is None:
scale = TW / W
elif TW is None and TH is not None:
scale = TH / H
elif TW is not None and TH is not None:
SW = W / TW
SH = H / TH
scale = 1.0
if SW > 1.0 or SH > 1.0 or (SW < 1.0 and SH < 1.0):
scale /= max(SW, SH)
else:
raise ValueError('TW or TH should be specified')
if not allow_upscale and scale > 1.0:
scale = 1.0
if scale != 1.0:
img = img.transpose( (1,2,0,3) ).reshape( (H,W,N*C) )
img = cv2.resize (img, ( int(W*scale), int(H*scale) ), interpolation=ImageProcessor.Interpolation.LINEAR)
H,W = img.shape[0:2]
img = img.reshape( (H,W,N,C) ).transpose( (2,0,1,3) )
if pad_to_target:
w_pad = (TW-W) if TW is not None else 0
h_pad = (TH-H) if TH is not None else 0
if w_pad != 0 or h_pad != 0:
img = np.pad(img, ( (0,0), (0,h_pad), (0,w_pad), (0,0) ))
self._img = img
return scale
def clip(self, min, max) -> 'ImageProcessor':
np.clip(self._img, min, max, out=self._img)
return self
def clip2(self, low_check, low_val, high_check, high_val) -> 'ImageProcessor':
img = self._img
l, h = img < low_check, img > high_check
img[l] = low_val
img[h] = high_val
return self
def reresize(self, power : float, interpolation : 'ImageProcessor.Interpolation' = None, mask = None) -> 'ImageProcessor':
"""
power float 0 .. 1.0
"""
power = min(1, max(0, power))
if power == 0:
return self
if interpolation is None:
interpolation = ImageProcessor.Interpolation.LINEAR
img = orig_img = self._img
N,H,W,C = img.shape
W_lr = max(4, int(W*(1.0-power)))
H_lr = max(4, int(H*(1.0-power)))
img = img.transpose( (1,2,0,3) ).reshape( (H,W,N*C) )
img = cv2.resize (img, (W_lr,H_lr), interpolation=_cv_inter[interpolation])
img = cv2.resize (img, (W,H) , interpolation=_cv_inter[interpolation])
img = img.reshape( (H,W,N,C) ).transpose( (2,0,1,3) )
if mask is not None:
mask = self._check_normalize_mask(mask)
img = ne.evaluate('orig_img*(1-mask) + img*mask').astype(orig_img.dtype)
self._img = img
return self
def box_sharpen(self, size : int, power : float, mask = None) -> 'ImageProcessor':
"""
size int kernel size
power float 0 .. 1.0 (or higher)
"""
power = max(0, power)
if power == 0:
return self
if size % 2 == 0:
size += 1
dtype = self.get_dtype()
self.to_ufloat32()
img = orig_img = self._img
N,H,W,C = img.shape
img = img.transpose( (1,2,0,3) ).reshape( (H,W,N*C) )
kernel = np.zeros( (size, size), dtype=np.float32)
kernel[ size//2, size//2] = 1.0
box_filter = np.ones( (size, size), dtype=np.float32) / (size**2)
kernel = kernel + (kernel - box_filter) * (power)
img = cv2.filter2D(img, -1, kernel)
img = np.clip(img, 0, 1, out=img)
img = img.reshape( (H,W,N,C) ).transpose( (2,0,1,3) )
if mask is not None:
mask = self._check_normalize_mask(mask)
img = ne.evaluate('orig_img*(1-mask) + img*mask')
self._img = img
self.to_dtype(dtype)
return self
def gaussian_sharpen(self, sigma : float, power : float, mask = None) -> 'ImageProcessor':
"""
sigma float
power float 0 .. 1.0 and higher
"""
sigma = max(0, sigma)
if sigma == 0:
return self
dtype = self.get_dtype()
self.to_ufloat32()
img = orig_img = self._img
N,H,W,C = img.shape
img = img.transpose( (1,2,0,3) ).reshape( (H,W,N*C) )
img = cv2.addWeighted(img, 1.0 + power,
cv2.GaussianBlur(img, (0, 0), sigma), -power, 0)
img = np.clip(img, 0, 1, out=img)
img = img.reshape( (H,W,N,C) ).transpose( (2,0,1,3) )
if mask is not None:
mask = self._check_normalize_mask(mask)
img = ne.evaluate('orig_img*(1-mask) + img*mask')
self._img = img
self.to_dtype(dtype)
return self
def gaussian_blur(self, sigma : float, opacity : float = 1.0, mask = None) -> 'ImageProcessor':
"""