From a95e53a69f45d10e9b720e05832543d82df348f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20L=C3=A1n?= Date: Mon, 3 Apr 2023 23:00:22 +0200 Subject: [PATCH] Reworked core filters, simplified naming, parameters, removed unneccesary ones. --- src/filters.py | 467 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 302 insertions(+), 165 deletions(-) diff --git a/src/filters.py b/src/filters.py index 09320ff..ec8a571 100644 --- a/src/filters.py +++ b/src/filters.py @@ -7,121 +7,296 @@ import numpy as np import cv2 as cv from skimage import filters as skiflt from skimage import restoration as skirest -#from scipy import signal as sig +from skimage import morphology as skimorph +# from scipy import signal as sig from PIL import Image, ImageFilter +import bm3d -# Parent class for all the filters class filter: ''' Parent class for all the filters. ''' def __init__(self, img): + ''' + :param img: Image to be filtered + ''' self.img = img -class convolve(filter): - ''' Convolve with custom kernel using opencv. - If no kernel is given, use default 3x3 kernel for averaging. - Possibly useful for custom filters. +# --------------------- DENOISING FILTERS ---------------------# + + +class gaussian(filter): + '''Gaussian blur filter from scikit-image. + Easier to use than opencv version. ''' def __init__(self, img): super().__init__(img) def apply(self, params): - ksize = int(params["ksize"]) if params["ksize"] else 3 - kernel = np.array(params["kernel"]) if params["kernel"] else np.ones( - (ksize, ksize), np.float32) / np.sqrt(ksize) + + # Standard deviation for Gaussian kernel + sigma = float(params["sigma"]) if params["sigma"] else 1 - #print("with params: ksize: " + - # str(ksize) + " kernel: \n" + str(kernel)) - self.img = cv.filter2D(self.img, -1, kernel) + print("with params: sigma: " + str(sigma)) + self.img = skiflt.gaussian( + self.img, sigma=sigma, preserve_range=True) -class blur(filter): - ''' Blur filter from OpenCV. - Performs averaging of the image. +class median(filter): + ''' Median blur filter from scikit-image. + Using this over opencv version as that one is limited to 5x5 kernel. ''' + def __init__(self, img): super().__init__(img) def apply(self, params): - # TODO remove try-except - if(params["anchor"]): - try: - anchor = tuple(map(int, params["anchor"].split(','))) - except AttributeError: - anchor = tuple(params["anchor"]) - else: - anchor = (-1, -1) + # Size of the median filter kernel + # Used kernel is disk of size ksize ksize = int(params["ksize"]) if params["ksize"] else 3 - #print("with params: ksize: " + - # str(ksize) + " anchor: " + str(anchor)) - self.img = cv.blur(self.img, ksize=(ksize, ksize), anchor=anchor) + print("with params: ksize: " + str(ksize)) + self.img = skiflt.median(self.img, footprint=skimorph.disk(ksize)) -class gaussian(filter): - ''' Gaussian blur filter from OpenCV. +class bilateral(filter): + ''' Bilateral filter from opencv. ''' + def __init__(self, img): super().__init__(img) def apply(self, params): - ksize = int(params["ksize"]) if params["ksize"] else 3 - sigmaX = float(params["sigmaX"]) if params["sigmaX"] else 0 - sigmaY = float(params["sigmaY"]) if params["sigmaY"] else 0 - - #print("with params: ksize: " + str(ksize) + - # " sigmaX: " + str(sigmaX) + " sigmaY: " + str(sigmaY)) - self.img = cv.GaussianBlur(self.img, (ksize, ksize), sigmaX, sigmaY) + # Diameter of pixel neighborhood used for filtering + # This determines how fast the filtering is going to be + diameter = int(params["diameter"]) if params["diameter"] else 3 + # Standard deviation for grayvalue/color distance + # A larger value results in averaging of pixels with larger radiometric differences + sigmaColor = int(params["sigmaColor"]) if params["sigmaColor"] else 75 -class median(filter): - ''' Median blur filter from scikit-image. - Using this over opencv version as that one is limited to 5x5 kernel. + # Standard deviation for range distance in pixels + # A larger value results in averaging of pixels with larger spatial differences + sigmaSpace = int(params["sigmaSpace"]) if params["sigmaSpace"] else 75 + + print("with params: diameter: " + str(diameter) + " sigmaColor: " + + str(sigmaColor) + " sigmaSpace: " + str(sigmaSpace)) + self.img = np.uint8(self.img) + self.img = cv.bilateralFilter( + self.img, diameter, sigmaColor, sigmaSpace) + + +class bilateral_scikit(filter): + ''' Skimage denoise_bilateral filter. + Averages pixels based on their distance and color similarity. + Preserves edges while removing unwanted noise. + Much slower than opencv implementation. ''' + def __init__(self, img): super().__init__(img) def apply(self, params): - ksize = int(params["ksize"]) if params["ksize"] else 3 - #print("with params: ksize: " + str(ksize)) - self.img = skiflt.median(self.img, footprint=np.ones((ksize, ksize))) + # Standard deviation for grayvalue/color distance. + # A larger value results in averaging of pixels with larger radiometric differences. + # Range of values: 0 to 1. + sigmaColor = float(params["sigmaColor"] + ) if params["sigmaColor"] else 0.1 + + # Standard deviation for range distance in pixels. + # A larger value results in averaging of pixels with larger spatial differences. + # Range of values: 0 to image size, recommend not more than 20 pixels. + sigmaSpace = float(params["sigmaSpace"] + ) if params["sigmaSpace"] else 9.0 + print("with params: sigma_color: " + str(sigmaColor) + + " sigma_spatial: " + str(sigmaSpace)) + self.img = skirest.denoise_bilateral( + self.img, sigma_color=sigmaColor, sigma_spatial=sigmaSpace) + self.img = np.uint8(self.img * 255.0) # converting back to uint8 -class bilateral(filter): - ''' Bilateral filter from OpenCV. + +class nlmeans(filter): + ''' Non-local means filter from scikit-image. ''' + def __init__(self, img): super().__init__(img) def apply(self, params): - # Set default values - d = int(params["d"]) if params["d"] else 1 - sigmaColor = int(params["sigmaColor"]) if params["sigmaColor"] else 75 - sigmaSpace = int(params["sigmaSpace"]) if params["sigmaSpace"] else 75 - #print("with params: d: " + str(d) + " sigmaColor: " + - # str(sigmaColor) + " sigmaSpace: " + str(sigmaSpace)) - self.img = np.uint8(self.img) - self.img = cv.bilateralFilter(self.img, d, sigmaColor, sigmaSpace) + # Size of patches used for denoising + patch_size = int(params["patch_size"]) if params["patch_size"] else 5 + + # Distance in pixels where to search patches + patch_distance = int(params["patch_distance"] + ) if params["patch_distance"] else 3 + + # Estimated standard deviation of the noise + sigma = np.mean(skirest.estimate_sigma(self.img)) + + # Cut-off distance, higher means more smoothed image + h = float(params["h"])*sigma if params["h"] else 0.1*sigma + + print("with params: patch_size: " + str(patch_size) + " patch_distance: " + + str(patch_distance) + " h: " + str(round(h, 4))) + self.img = skirest.denoise_nl_means( + self.img, patch_size=patch_size, fast_mode=True, patch_distance=patch_distance, h=h) + self.img = np.uint8(self.img * 255.0) # converting back to uint8 + + +class total_variation(filter): + ''' Scikit image denoise_tv_chambolle filter from scikit-image. + + Performs total variation denoising technique based on original Chambolle paper. + This filter removes fine detail, but preserves details such as edges. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + # Denoising weight. Larger values result in more denoising. + weight = float(params["weight"]) if params["weight"] else 0.1 + + print("with params: weight: " + str(weight)) + self.img = skirest.denoise_tv_chambolle( + self.img, weight=weight) + self.img = np.uint8(self.img * 255.0) # converting back to uint8 + + +class block_match(filter): + '''Block matching filter from bm3d. + + This filter is very slow and should be used only on small images + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + sigma = float(params["sigma"]) if params["sigma"] else 20 + + print("with params: sigma: " + str(sigma)) + self.img = bm3d.bm3d(self.img, sigma_psd=sigma, + stage_arg=bm3d.BM3DStages.ALL_STAGES) + + +class unsharp_mask_scikit(filter): + ''' Unsharp mask filter from scikit. + + Apply blurring using gaussian filter, then subtract the blurred image from the original image. + Radius parameter is the sigma parameter of the gaussian filter. + Amount parameter regulates the strength of the unsharp mask. + Better results than using this from opencv. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + # radius of the gaussian filter + radius = int(params["radius"]) if params["radius"] else 3 + # strength of the unsharp mask + amount = float(params["amount"]) if params["amount"] else 1.0 + + print("with params: radius: " + + str(radius) + " amount: " + str(amount)) + self.img = skiflt.unsharp_mask(self.img, radius=radius, + amount=amount, channel_axis=None) + self.img = np.uint8(self.img * 255.0) # converting back to uintknapsack + +# ------------------- EDGE DETECTION FILTERS -------------------# + + +class farid(filter): + ''' Farid filter from filters. + Not sure what this might be used for yet. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + self.img = skiflt.farid(self.img) + +# ------------------ RIDGE EXTRACTION FILTERS ------------------# + + +class meijering(filter): + ''' Meijering filter from scikit-image filters. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + self.img = skiflt.meijering(self.img) + self.img = np.uint8(self.img * 255.0) + + +class sato(filter): + ''' Meijering filter from scikit-image filters. + Exctracts black ridges. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + self.img = skiflt.sato(self.img) + self.img = np.uint8(self.img * 255.0) + + +class hessian(filter): + ''' Hessian filter from scikit-image filters. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + sigmas = float(params["sigma"]) if params["sigma"] else 1.2 + + self.img = skiflt.hessian(self.img, sigmas=sigmas) + self.img = np.uint8(self.img * 255.0) + +# ------------------- MISCELLANEOUS FILTERS -------------------# + + +class invert(filter): + ''' Invert the image using bitwise_not from opencv. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + self.img = cv.bitwise_not(self.img) class scale_values(filter): ''' Scale values of the image to use the entire range of data type. This should remove the line height issues. ''' + def __init__(self, img): super().__init__(img) def apply(self, params): - # do this once for inverted image and once for original - # this is done to get whiter whites and blacker blacks - # which helps to get exact line height on stl model + # scale once for inverted image and once for original + # this is done to get the full value range of the data type + # which might help getting exact line height on stl model tmp = cv.bitwise_not(self.img.astype(np.uint8)) coef = 255 / np.max(tmp) tmp = tmp * coef @@ -139,8 +314,8 @@ class binarize(filter): maxval = int(params["maxval"]) if params["maxval"] else 255 type = int(params["type"]) if params["type"] else 0 - #print("with params: threshold: " + str(threshold) + - # " maxval: " + str(maxval) + " type: " + str(type)) + print("with params: threshold: " + str(threshold) + + " maxval: " + str(maxval) + " type: " + str(type)) self.img = cv.threshold(self.img, threshold, maxval, type)[1] @@ -151,96 +326,75 @@ class add_margin(filter): def apply(self, params): margin = int(params["margin"]) if params["margin"] else 10 color = int(params["color"]) if params["color"] else 255 + + print("with params: margin: " + str(margin) + " color: " + str(color)) + self.fig.set_size_inches( ((self.width + 2 * margin) / self.dpi, (self.height + 2 * margin) / self.dpi)) self.img = cv.copyMakeBorder( self.img, margin, margin, margin, margin, cv.BORDER_CONSTANT, value=color) self.height, self.width = self.img.shape - -class denoise(filter): - # TODO possibly not necessary - def __init__(self, img): - super().__init__(img) - - def apply(self, params): - h = int(params["h"]) if params["h"] else 10 - tWS = int(params["templateWindowSize"] - ) if params["templateWindowSize"] else 7 - sWS = int(params["searchWindowSize"] - ) if params["searchWindowSize"] else 21 +# ---------------------- OLD --------------------------# - #print("with params: h: " + str(h) + - # " tWS: " + str(tWS) + " sWS: " + str(sWS)) - self.img = np.uint8(self.img) - self.img = cv.fastNlMeansDenoising( - self.img, h, tWS, sWS) +# TODO: REVISE, REMOVE unused filters -class denoise_bilateral(filter): - ''' Scikit image denoise_bilateral filter. - Performs bilateral denoising technique on the image. - Averages pixels based on their distance and color similarity. - Preserves edges while removing unwanted noise. +class convolve(filter): + ''' Convolve with custom kernel using opencv. + If no kernel is given, use default 3x3 kernel for averaging. + Possibly useful for custom filters. ''' def __init__(self, img): super().__init__(img) def apply(self, params): + kernel = np.array(params["kernel"]) if params["kernel"] else np.ones( + (3, 3), np.float32) / 9 - # Standard deviation for grayvalue/color distance. - # A larger value results in averaging of pixels with larger radiometric differences. - sigmaColor = float(params["sigmaColor"] - ) if params["sigmaColor"] else 0.1 + print("with params: kernel: \n" + str(kernel)) + self.img = cv.filter2D(self.img, -1, kernel) - # Standard deviation for range distance. - # A larger value results in averaging of pixels with larger spatial differences. - - sigmaSpace = float(params["sigmaSpace"] - ) if params["sigmaSpace"] else 15.0 - # Repetition of filter application. - iterations = int(params["iterations"]) if params["iterations"] else 1 - #print("with params: sigma_color: " + str(sigmaColor) + - # " sigma_spatial: " + str(sigmaSpace) + " iterations: " + str(iterations)) +class blur(filter): + ''' Blur filter from opencv. + Performs averaging of the image. + ''' - for i in range(iterations): - self.img = skirest.denoise_bilateral( - self.img, sigma_color=sigmaColor, - sigma_spatial=sigmaSpace, channel_axis=None) + def __init__(self, img): + super().__init__(img) + def apply(self, params): + ksize = int(params["ksize"]) if params["ksize"] else 3 -class denoise_tv_chambolle(filter): - ''' Scikit image denoise_tv_chambolle filter from scikit-image. + print("with params: ksize: " + str(ksize)) + self.img = cv.blur(self.img, ksize=(ksize, ksize)) - Performs total variation denoising technique on the image. - This filter removes fine detail, but preserves edges. - ''' +class denoise(filter): def __init__(self, img): super().__init__(img) def apply(self, params): + h = int(params["h"]) if params["h"] else 10 + tWS = int(params["templateWindowSize"] + ) if params["templateWindowSize"] else 7 + sWS = int(params["searchWindowSize"] + ) if params["searchWindowSize"] else 21 - # Denoising weight. The greater weight, the more denoising. - weight = float(params["weight"]) if params["weight"] else 0.1 - - # Maximal number of iterations used for the optimization. - iterations = int(params["iterations"]) if params["iterations"] else 1 - - #print("with params: weight: " + str(weight) + - # " iterations: " + str(iterations)) - for i in range(iterations): - self.img = skirest.denoise_tv_chambolle( - self.img, weight=weight, channel_axis=None) + # print("with params: h: " + str(h) + + # " tWS: " + str(tWS) + " sWS: " + str(sWS)) + self.img = np.uint8(self.img) + self.img = cv.fastNlMeansDenoising( + self.img, h, tWS, sWS) class sharpen(filter): ''' Convolution with a sharpening kernel using opencv. ''' - # TODO possibly unnecessary, because unsharp masking is working better + def __init__(self, img): super().__init__(img) @@ -248,7 +402,7 @@ class sharpen(filter): kernel = np.matrix(params["kernel"]) if params["kernel"] else np.array( [[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) - #print("with params: kernel: \n" + str(kernel)) + # print("with params: kernel: \n" + str(kernel)) self.img = cv.filter2D(self.img, ddepth=-1, kernel=kernel) @@ -269,43 +423,16 @@ class unsharp_mask(filter): blurred = cv.medianBlur(self.img, ksize) lap = cv.Laplacian(blurred, cv.CV_32F) - #print("with params: strength: " + + # print("with params: strength: " + # str(strength) + " ksize: " + str(ksize)) self.img = blurred - strength*lap -class unsharp_mask_scikit(filter): - ''' Unsharp mask filter from scikit. - - Apply blurring using gaussian filter, then subtract the blurred image from the original image. - Radius parameter is the sigma parameter of the gaussian filter. - Amount parameter regulates the strength of the unsharp mask. - Better results than using opencv module. - ''' - - def __init__(self, img): - super().__init__(img) - - def apply(self, params): - radius = int(params["radius"]) if params["radius"] else 3 - amount = float(params["amount"]) if params["amount"] else 1 - # TODO: i have no idea what this is or how to use it - channelAxis = int(params["channelAxis"] - ) if params["channelAxis"] else None - - #self.img = cv.cvtColor(self.img, cv.COLOR_GRAY2RGB) - #print("with params: radius: " + - # str(radius) + " amount: " + str(amount)) - self.img = skiflt.unsharp_mask( - self.img, radius=radius, amount=amount, channel_axis=channelAxis) - #self.img = cv.cvtColor(self.img, cv.COLOR_RGB2GRAY) - - class unsharp_mask_pil(filter): ''' Unsharp mask filter from PIL. ''' - # TODO: does not work + def __init__(self, img): super().__init__(img) @@ -320,7 +447,7 @@ class unsharp_mask_pil(filter): # Threshold controls the minimum brightness change that will be sharpened threshold = int(params["threshold"]) if params["threshold"] else 3 - #print("with params: radius: " + + # print("with params: radius: " + # str(radius) + " percent: " + str(percent) + " threshold: " + str(threshold)) self.img = np.uint8(self.img) tmp = Image.fromarray(self.img) @@ -328,7 +455,27 @@ class unsharp_mask_pil(filter): self.img = np.asarray(tmp) -class morph(filter): +class erode(filter): + ''' General morphological operations from OpenCV. + + Can be used with MORPH_OPEN, MORPH_CLOSE, MORPH_DILATE, MORPH_ERODE and more as 'op'. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + + # get an ellipse kernel + kernel = np.matrix(params["kernel"]) if params["kernel"] else cv.getStructuringElement( + cv.MORPH_ELLIPSE, (3, 3)) + + print("with params: kernel: \n" + str(kernel)) + self.img = cv.morphologyEx( + np.uint8(self.img), op=cv.MORPH_ERODE, kernel=kernel) + + +class dilate(filter): ''' General morphological operations from OpenCV. Can be used with MORPH_OPEN, MORPH_CLOSE, MORPH_DILATE, MORPH_ERODE and more as 'op'. @@ -338,25 +485,14 @@ class morph(filter): super().__init__(img) def apply(self, params): - # TODO: this is probably better with binarized image - kernel = np.matrix(params["kernel"]) if params["kernel"] else np.ones( - (3, 3), np.uint8) - iterations = int(params["iterations"]) if params["iterations"] else 1 - op = getattr(cv, params["op"]) if params["op"] else cv.MORPH_OPEN - if(params["anchor"]): - # TODO remove try-except - try: - anchor = tuple(map(int, params["anchor"].split(','))) - except AttributeError: - anchor = tuple(params["anchor"]) - else: - anchor = (-1, -1) - - #print("with params: kernel: \n" + str(kernel) + " anchor: " + - # str(anchor) + " iterations: " + str(iterations) + " op: " + str(op)) + + # get an ellipse kernel + kernel = np.matrix(params["kernel"]) if params["kernel"] else cv.getStructuringElement( + cv.MORPH_ELLIPSE, (3, 3)) + + print("with params: kernel: \n" + str(kernel)) self.img = cv.morphologyEx( - np.uint8(self.img), op=op, kernel=kernel, - anchor=anchor, iterations=iterations) + np.uint8(self.img), op=cv.MORPH_DILATE, kernel=kernel) class gabor(filter): @@ -368,11 +504,11 @@ class gabor(filter): def __init__(self, img): super().__init__(img) - # TODO: not working properly def apply(self, params): ksize = int(params["ksize"]) if params["ksize"] else 31 sigma = float(params["sigma"]) if params["sigma"] else 10.0 - theta = params["theta"] if params["theta"] else [0,np.pi/16,np.pi-np.pi/16] + theta = params["theta"] if params["theta"] else [ + 0, np.pi/16, np.pi-np.pi/16] lambd = float(params["lambd"]) if params["lambd"] else 10.0 gamma = float(params["gamma"]) if params["gamma"] else 0.02 psi = float(params["psi"]) if params["psi"] else 0.0 @@ -380,7 +516,8 @@ class gabor(filter): filters = [] for i in range(len(theta)): - g_kernel = cv.getGaborKernel(ksize=(ksize, ksize), sigma=sigma, theta=theta[i], lambd=lambd, gamma=gamma, psi=psi) + g_kernel = cv.getGaborKernel(ksize=( + ksize, ksize), sigma=sigma, theta=theta[i], lambd=lambd, gamma=gamma, psi=psi) g_kernel = g_kernel / 1.5 * g_kernel.sum() filters.append(g_kernel)