diff --git a/src/filters.py b/src/filters.py index b4769d8..d241f8c 100644 --- a/src/filters.py +++ b/src/filters.py @@ -1,17 +1,18 @@ """! @file filters.py -@brief Filter library for the application -@author xlanro00 + @brief Filter library for the application + @author xlanro00 """ import numpy as np #import matplotlib.pyplot as plt import cv2 as cv +from skimage import filters as skiflt +from skimage import restoration as skirest # Parent class for all the filters class filter: - ''' - Parent class for all the filters + ''' Parent class for all the filters. ''' def __init__(self, img): @@ -19,8 +20,9 @@ class filter: class convolve(filter): - ''' Convolve using custom kernel, - if no kernel is given, use default 3x3 kernel for averaging + ''' 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): @@ -31,15 +33,20 @@ class convolve(filter): kernel = np.array(params["kernel"]) if params["kernel"] else np.ones( (ksize, ksize), np.float32) / np.sqrt(ksize) - #print("with params: " + " ksize: " + str(ksize) + " kernel: \n" + str(kernel)) + print("with params: ksize: " + + str(ksize) + " kernel: \n" + str(kernel)) self.img = cv.filter2D(self.img, -1, kernel) class blur(filter): + ''' Blur filter from OpenCV. + Performs averaging of the image. + ''' 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(','))) @@ -49,11 +56,14 @@ class blur(filter): anchor = (-1, -1) ksize = int(params["ksize"]) if params["ksize"] else 3 - #print("with params: " + " ksize: " + str(ksize) + " anchor: " + str(anchor)) + print("with params: ksize: " + + str(ksize) + " anchor: " + str(anchor)) self.img = cv.blur(self.img, ksize=(ksize, ksize), anchor=anchor) class gaussian(filter): + ''' Gaussian blur filter from OpenCV. + ''' def __init__(self, img): super().__init__(img) @@ -62,22 +72,27 @@ class gaussian(filter): 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)) + print("with params: ksize: " + str(ksize) + + " sigmaX: " + str(sigmaX) + " sigmaY: " + str(sigmaY)) self.img = cv.GaussianBlur(self.img, (ksize, ksize), sigmaX, sigmaY) class median(filter): + ''' Median blur filter from OpenCV. + ''' 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 = cv.medianBlur(np.uint8(self.img), ksize) + print("with params: ksize: " + str(ksize)) + self.img = cv.medianBlur(np.float32(self.img), ksize) class bilateral(filter): + ''' Bilateral filter from OpenCV. + ''' def __init__(self, img): super().__init__(img) @@ -87,27 +102,87 @@ class bilateral(filter): 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)) + print("with params: d: " + str(d) + " sigmaColor: " + + str(sigmaColor) + " sigmaSpace: " + str(sigmaSpace)) self.img = cv.bilateralFilter(self.img, d, sigmaColor, sigmaSpace) 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 20 + 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 - #print("with params: " + " h: " + str(h) + " tWS: " + str(tWS) + " sWS: " + str(sWS)) + 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) + self.img = cv.fastNlMeansDenoising( + self.img, h, tWS, sWS) + + +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. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + sigmaColor = float(params["sigmaColor"] + ) if params["sigmaColor"] else 0.1 + sigmaSpace = float(params["sigmaSpace"] + ) if params["sigmaSpace"] else 15.0 + channelAxis = int(params["channelAxis"] + ) if params["channelAxis"] else None + iterations = int(params["iterations"]) if params["iterations"] else 1 + + print("with params: sigma_color: " + str(sigmaColor) + + " sigma_spatial: " + str(sigmaSpace) + " channel_axis: " + + str(channelAxis) + " iterations: " + str(iterations)) + + for i in range(iterations): + self.img = skirest.denoise_bilateral( + self.img, sigma_color=sigmaColor, + sigma_spatial=sigmaSpace, channel_axis=channelAxis) + + +class denoise_tv_chambolle(filter): + ''' Scikit image denoise_tv_chambolle filter from scikit-image. + + Performs total variation denoising technique on the image. + This filter removes fine detail, but preserves edges. + ''' + + def __init__(self, img): + super().__init__(img) + + def apply(self, params): + weight = float(params["weight"]) if params["weight"] else 0.1 + channelAxis = int(params["channelAxis"] + ) if params["channelAxis"] else None + iterations = int(params["iterations"]) if params["iterations"] else 1 + + print("with params: weight: " + str(weight) + + " channel_axis: " + str(channelAxis) + " iterations: " + str(iterations)) + for i in range(iterations): + self.img = skirest.denoise_tv_chambolle( + self.img, weight=weight, channel_axis=channelAxis) 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) @@ -115,13 +190,13 @@ 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) class unsharp_mask(filter): - ''' Unsharp mask filter. - + ''' Unsharp mask filter from opencv. + First blur the image a little bit, then calculate Laplacian of the image to get the edges. Scale the Laplacian and subtract it from the original image. ''' @@ -132,13 +207,43 @@ class unsharp_mask(filter): def apply(self, params): strength = float(params["strength"]) if params["strength"] else 1.0 ksize = int(params["ksize"]) if params["ksize"] else 3 - blurred = cv.medianBlur(np.uint8(self.img), ksize) + + blurred = cv.medianBlur(self.img, ksize) lap = cv.Laplacian(blurred, cv.CV_32F) + + 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 + 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 morph(filter): - ''' General morphological operations. + ''' General morphological operations from OpenCV. Can be used with MORPH_OPEN, MORPH_CLOSE, MORPH_DILATE, MORPH_ERODE and more as 'op'. ''' @@ -152,6 +257,7 @@ class morph(filter): 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: @@ -159,6 +265,8 @@ class morph(filter): else: anchor = (-1, -1) - #print("with params: " + " kernel: \n" + str(kernel) + " anchor: " + str(anchor) + " iterations: " + str(iterations) + " op: " + str(op)) + print("with params: kernel: \n" + str(kernel) + " anchor: " + + str(anchor) + " iterations: " + str(iterations) + " op: " + str(op)) self.img = cv.morphologyEx( - self.img, op=op, kernel=kernel, anchor=anchor, iterations=iterations) + np.uint8(self.img), op=op, kernel=kernel, + anchor=anchor, iterations=iterations) diff --git a/src/main.py b/src/main.py index a25a030..eba7fc4 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,12 @@ """! @file main.py -@brief Main file for the application -@author xlanro00 + @brief Main file for the application + @author xlanro00 """ # Import basic libraries import argparse as ap import sys import json -#from datetime import datetime # Libraries for image processing import numpy as np @@ -33,7 +32,6 @@ class apply_filters: self.config_file = self.args.config[0] self.preset_name = self.args.config[1] self.config = json.load(open(self.config_file)) - print("Config loaded") self.parse_conf() # If no config file given, expect filters in command line @@ -63,11 +61,12 @@ class apply_filters: def run(self): # read as numpy.array - self.img = cv.imread(self.input_file, cv.IMREAD_GRAYSCALE) + self.img = cv.imread( + self.input_file, cv.IMREAD_GRAYSCALE).astype(np.uint8) self.width = self.img.shape[1] self.height = self.img.shape[0] - print(self.width, self.height) + self.print_size(self.img.shape) fig = plt.figure(figsize=(self.width, self.height), frameon=False, dpi=self.dpi / 100) # dpi is in cm @@ -86,21 +85,27 @@ class apply_filters: if self.args.stl: self.make_lithophane() - def parse_params(self, params): + ''' Parse parameters of filters. + Set to None if not given. + They are later set in the filter method. + ''' + possible_params = {"h", "searchWindowSize", "templateWindowSize", "ksize", "kernel", "sigmaX", "sigmaY", - "sigmaColor", "sigmaSpace", "d", "anchor", "iterations", - "op", "strength"} + "sigmaColor", "sigmaSpace", "d", "anchor", "iterations", + "op", "strength", "amount", "radius", "weight", "channelAxis"} for key in possible_params: - try: - params[key] = params[key] - except KeyError: + if params.get(key) is None: params[key] = None + else: + params[key] = params[key] def parse_conf(self): - # Parse configuration file if given. - try: + ''' Parse configuration file if one was given and store filters with their parameters + ''' + + if self.preset_name in self.config: filter_array = self.config[self.preset_name] for i, filter in enumerate(range(len(filter_array)), start=1): self.filters.append(filter_array[filter]["name"]) @@ -109,13 +114,15 @@ class apply_filters: if attribute != "name": self.params[i][attribute] = value self.parse_params(self.params[i]) - - except(KeyError): + print("Loaded preset: " + self.preset_name + + " from file: " + self.config_file) + else: print("Preset not found", file=sys.stderr) def parse_arguments(self): + ''' Parse arguments from command line + ''' - # Parse arguments parser = ap.ArgumentParser(prog='main.py', description='Program for processing a 2D image into 3D fingerprint.', usage='%(prog)s [-h] [-m | --mirror | --no-mirror] input_file output_file dpi ([-c config_file preset | --config config_file preset] | [filters ...])') @@ -127,10 +134,11 @@ class apply_filters: help="output file location") parser.add_argument("dpi", type=int, help="scanner dpi") - # boolean switch + # boolean switch argument parser.add_argument('-m', "--mirror", help="mirror input image", type=bool, action=ap.BooleanOptionalAction) + # another boolean switch argument parser.add_argument('-s', '--stl', help="make stl model from processed image", type=bool, action=ap.BooleanOptionalAction) @@ -149,7 +157,7 @@ class apply_filters: ''' Selects filter method of filters library. ''' - print("Applying " + filter_name + " filter", file=sys.stderr) + print("Applying " + filter_name + " filter ", end='') return getattr(flt, filter_name) def resize_image(self): @@ -163,16 +171,16 @@ class apply_filters: should be used only if we want a positive model ''' - #TODO make this automatic for positive STL + # TODO make this automatic for positive STL print("Mirroring image", file=sys.stderr) self.img = cv.flip(self.img, 1) # 1 for vertical mirror def apply_filter(self): ''' Apply filters to image. - + Applies the filters one by one, if no filters were given, just save original image output. ''' - + if len(self.filters) == 0: # No filter given, just save the image pass @@ -180,11 +188,13 @@ class apply_filters: # Apply all filters for i, filter_name in enumerate(self.filters): filter = self.filter_factory(filter_name) + # print(self.img.dtype) filter.apply(self, self.params[i+1]) + # print(self.img.dtype) def print_size(self, size): - print("Width: " + str(size[0]), file=sys.stderr) - print("Height: " + str(size[1]), file=sys.stderr) + print("Height: " + str(size[0]), file=sys.stderr) + print("Width: " + str(size[1]), file=sys.stderr) def save_image(self, fig, ax): ''' Save processed image. @@ -196,7 +206,6 @@ class apply_filters: fig.savefig(fname=self.output_file) def make_lithophane(self): - pass '''After processing image, make a lithophane from it. ''' @@ -208,15 +217,21 @@ class apply_filters: self.save_model() def make_meshgrid(self): - + ''' Create numpy meshgrid. + Modify image values to get more usable depth values. + Add zero padding to image to make sides of the plate. + ''' # Modify image to make it more suitable depth # values1 = (1 + (1 - self.img/255)/6) * 255/10 # this works - # values2 = (1 - (1 - self.img/255)/6) * 255/10 # TODO: i dont know how to make white surrounding be extruded + # values2 = (1 - (1 - self.img/255)/6) * 255/10 # + # TODO: i dont know how to make white surrounding be extruded + values1better = 28.05 - 0.01*self.img #values2better = 22.95 - 0.01*self.img # (np.around(values2[::300],3)) - # Add zero padding to image to make sides of the plate + # Add zero padding to image + # TODO this better be done in the next function to keep dimensions intact self.height = self.img.shape[0] + 2 self.width = self.img.shape[1] + 2 self.img = np.zeros([self.height, self.width]) @@ -228,6 +243,11 @@ class apply_filters: self.meshgrid = np.meshgrid(verticesX, verticesY) def make_mesh(self): + ''' Create mesh from image. + Create vertices from meshgrid, add depth values from image. + Create faces from vertices. + ''' + # Convert meshgrid and image matrix to array of 3D points vertice_arr = np.vstack(list(map(np.ravel, self.meshgrid))).T z = (self.img / 10).reshape(-1, 1) @@ -278,7 +298,10 @@ class apply_filters: self.model.vectors[i][j] = vertices[face[j], :] def save_model(self): - print("Saving stl model", file=sys.stderr) + ''' Save final model to stl file. + ''' + + print("Saving lithophane to stl file", file=sys.stderr) self.model.save('res/test.stl')