diff --git a/README.md b/README.md index c565a3d..bef1dfa 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Once all the requirements are installed, the program is ready to use. There are 1. manually list filter names and parameters from command line ```sh - python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_shell.png 600 total_variation weight=0.15 median ksize=5 + python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_cline.png 600 total_variation weight=0.15 median ksize=5 ``` 2. from preset saved in a json config file, that can be used to tune and modify existing presetrs, or create new ones @@ -74,7 +74,9 @@ Once all the requirements are installed, the program is ready to use. There are # Configuration and presets -There is an option to input the filter series as a preset from json configuration file. +There is an option to input the filter series as a preset from json configuration file. +This preset is automatically stored inside a json file, which serves as a database for storing filters. +This prevents losing filter preset information when modifying filter which was used to generate 3D models. @@ -127,7 +129,17 @@ There is an option to input the filter series as a preset from json configuratio
+There is also an option to save current command line setting as a preset using -d switch: +* General command for saving filter preset + ```sh + python3 src/main.py input_file output_file dpi -d new_preset_name filters + ``` + +* Working example + ```sh + python3 src/main.py res/examples/Palec_P4.tif res/examples/Palec_P4_from_cline.png 600 -d preset_gaussian gaussian sigma=1 + ``` All the filters used and their parameters are described below. diff --git a/src/config_parser.py b/src/config_parser.py index 35c09e5..44603ec 100644 --- a/src/config_parser.py +++ b/src/config_parser.py @@ -6,7 +6,7 @@ from os.path import exists import json import hashlib - +import log def save_preset(filters, params, preset_name): '''Save filter preset to database. @@ -46,7 +46,7 @@ def store_to_db(preset, preset_name): # If database doesn't exist, create it if not exists("db.json"): - print("Storing preset to database") + log.print_message("Storing preset to database") with open("db.json", 'w') as db: json.dump(preset, db) else: @@ -61,9 +61,9 @@ def store_to_db(preset, preset_name): # If preset already exists, skip it if preset_name in db_presets: - print("Preset already exists in database, skipping") + log.print_message("Preset already exists in database, skipping") else: - print("Storing preset to database") + log.print_message("Storing preset to database") db_presets.update(preset) # Finally write the updated entries to db file @@ -91,10 +91,10 @@ def parse_conf(preset_name, filters, params, config_file): if attribute != "name": params[i][attribute] = value parse_params(params[i]) - print("Loaded preset: " + preset_name + - " from file: " + config_file) + log.print_message("Loaded preset:", preset_name, + "from file:", config_file) else: - print("Preset not found") + log.print_message("Preset not found") def parse_params(params): diff --git a/src/filters.py b/src/filters.py index b9344a3..9c07059 100644 --- a/src/filters.py +++ b/src/filters.py @@ -41,7 +41,6 @@ class gaussian(filter): # Standard deviation for Gaussian kernel sigma = float(params["sigma"]) if params["sigma"] else 1 - print("with params: sigma: " + str(sigma)) self.img = skiflt.gaussian( self.img, sigma=sigma, preserve_range=True) @@ -59,7 +58,6 @@ class median(filter): # Used kernel is disk of size ksize ksize = int(params["ksize"]) if params["ksize"] else 3 - print("with params: ksize: " + str(ksize)) self.img = skiflt.median(self.img, footprint=skimorph.disk(ksize)) @@ -84,8 +82,6 @@ class bilateral(filter): # 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) @@ -115,8 +111,6 @@ class bilateral_scikit(filter): 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 @@ -144,8 +138,6 @@ class nlmeans(filter): # 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 @@ -166,7 +158,6 @@ class total_variation(filter): # 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 @@ -184,7 +175,6 @@ class block_match(filter): 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) @@ -207,8 +197,6 @@ class unsharp_mask_scikit(filter): # 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 uint @@ -315,8 +303,6 @@ 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)) self.img = cv.threshold(self.img, threshold, maxval, type)[1] @@ -338,8 +324,6 @@ class add_margin(filter): 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( @@ -365,7 +349,6 @@ class convolve(filter): kernel = np.array(params["kernel"]) if params["kernel"] else np.ones( (3, 3), np.float32) / 9 - print("with params: kernel: \n" + str(kernel)) self.img = cv.filter2D(self.img, -1, kernel) @@ -380,7 +363,6 @@ class blur(filter): def apply(self, params): ksize = int(params["ksize"]) if params["ksize"] else 3 - print("with params: ksize: " + str(ksize)) self.img = cv.blur(self.img, ksize=(ksize, ksize)) @@ -395,8 +377,6 @@ class denoise(filter): sWS = int(params["searchWindowSize"] ) if params["searchWindowSize"] else 21 - # 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) @@ -413,7 +393,6 @@ 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)) self.img = cv.filter2D(self.img, ddepth=-1, kernel=kernel) @@ -434,8 +413,6 @@ class unsharp_mask(filter): 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 @@ -458,8 +435,6 @@ 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: " + - # str(radius) + " percent: " + str(percent) + " threshold: " + str(threshold)) self.img = np.uint8(self.img) tmp = Image.fromarray(self.img) tmp = tmp.filter(ImageFilter.UnsharpMask(radius, percent, threshold)) @@ -481,7 +456,6 @@ class erode(filter): 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) @@ -501,7 +475,6 @@ class dilate(filter): 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_DILATE, kernel=kernel) diff --git a/src/log.py b/src/log.py new file mode 100644 index 0000000..dcf3b7f --- /dev/null +++ b/src/log.py @@ -0,0 +1,22 @@ +"""! @file log.py + @brief File with printing functions + @author xlanro00 +""" + +import sys + +def print_message(*args, **kwargs): + '''Print given message to stderr. + :param message: message to be printed + ''' + + print("APP:", *args, file=sys.stderr, **kwargs) + + +def error_exit(error_message): + '''Print given error message and exit the application. + :param message: error message to be printed + ''' + + print("ERROR:" + error_message, file=sys.stderr) + exit(1) diff --git a/src/main.py b/src/main.py index cb97d34..0bc1f8e 100644 --- a/src/main.py +++ b/src/main.py @@ -5,8 +5,6 @@ # Import basic libraries import argparse as ap -import sys -import json from os.path import exists import hashlib import math @@ -22,6 +20,7 @@ import trimesh.transformations as tmtra # Import custom image filter library import filters as flt import config_parser as cp +import log class app: '''Main class for the application. @@ -41,28 +40,30 @@ class app: cp.parse_conf(self.preset_name, self.filters, self.params, self.config_file) elif self.args.filters: - print("No config file given, using command line arguments") - i = 0 + filter_index = 0 + log.print_message( + "No config file given, using command line arguments") # Otherwise expect filters from command line for filter_part in self.args.filters: # If no '=' char in filter, it is a new filter name if filter_part.find('=') == -1: self.filters.append(filter_part) - i += 1 - self.params[i] = {} # create empty dict for params + filter_index += 1 + # create empty dict for params + self.params[filter_index] = {} # Otherwise it's a parameter for current filter else: key, value = filter_part.split('=') - self.params[i][key] = value + self.params[filter_index][key] = value - cp.parse_params(self.params[i]) + cp.parse_params(self.params[filter_index]) # If database flag is set, save filters to database as a new preset if self.args.database: cp.save_preset(self.filters, self.params, self.args.database[0]) else: - print("No filters given, saving original image") + log.print_message("No filters given, saving original image") # Set input and output file paths, dpi and mirror flag for easier readability self.input_file = self.args.input_file @@ -72,9 +73,8 @@ class app: if exists(self.input_file): self.run_filtering() - else: - self.error_exit("Input file " + self.input_file + + log.error_exit("Input file " + self.input_file + " does not exist") if self.args.stl: @@ -128,12 +128,12 @@ class app: if len(self.args.stl) < 3: self.height_line = 2 self.height_base = 10 - print( + log.print_message( "Warning: Too few arguments, using default values (10mm base, 2mm lines)") else: self.height_line = float(self.args.stl[1]) self.height_base = float(self.args.stl[2]) - print("Base height:", self.height_base, + log.print_message("Base height:", self.height_base, "mm, lines depth/height:", self.height_line, "mm") elif self.args.stl[0] == 'c': @@ -145,40 +145,31 @@ class app: self.height_base = 10 self.curv_rate_x = 2 self.curv_rate_y = 6 - print( - "Warning: Too few arguments, using default values (2mm lines, curvature 0.5 on x, 0.5 on y)") + log.print_message("Warning: Too few arguments, using default values (2mm lines, curvature 0.5 on x, 0.5 on y)") else: self.height_line = float(self.args.stl[1]) self.height_base = float(self.args.stl[2]) self.curv_rate_x = float(self.args.stl[3]) self.curv_rate_y = float(self.args.stl[4]) - print("Line height:", self.height_line, "mm, base height: ", self.height_base, - "mm, x axis curvature: ", self.curv_rate_x, ", y axis curvature:", self.curv_rate_y) + log.print_message("Line height:", self.height_line, "mm, base height:", self.height_base, + "mm, x axis curvature:", self.curv_rate_x, ", y axis curvature:", self.curv_rate_y) elif self.args.stl[0] == 'm': self.mode = "mapped" # TODO: add default values for mapped mode, add finger model? if len(self.args.stl) < 2: - print( + log.print_message( "Warning: Too few arguments, using default values") else: self.height_line = float(self.args.stl[1]) self.finger_model = self.args.stl[2] else: - self.error_exit("Unrecognized generation mode") + log.error_exit("Unrecognized generation mode") - print("Stl generation in ", self.mode) + log.print_message("Stl generation in", self.mode, "mode") self.run_stl() - def error_exit(self, message): - '''Print given error message and exit the application. - :param message: error message to be printed - ''' - - print("ERROR:", message, file=sys.stderr) - exit(1) - # ------------------------- FILTERING -------------------------# def run_filtering(self): @@ -216,7 +207,7 @@ class app: '''Mirror image using opencv, should be used if we want a positive model. ''' - print("Mirroring image", file=sys.stderr) + log.print_message("Mirroring image") self.img = cv.flip(self.img, 1) # 1 for vertical mirror def apply_filters(self): @@ -228,7 +219,12 @@ class app: for i, filter_name in enumerate(self.filters): # Get filter class from filter.py, use the apply method filter = getattr(flt, filter_name) - print("Applying filter:", filter_name, file=sys.stderr) + log.print_message("Applying filter:", filter_name) + for param in self.params[i+1]: + if self.params[i+1][param] is not None: + log.print_message("\twith parameter", param, + "=", str(self.params[i+1][param])) + filter.apply(self, self.params[i+1]) else: pass @@ -237,7 +233,7 @@ class app: '''Save processed image to the output file. ''' - print("Saving image to", self.output_file, file=sys.stderr) + log.print_message("Saving image to", self.output_file) # Colormap must be set to grayscale to avoid color mismatch. ax.imshow(self.img, cmap="gray") @@ -253,7 +249,7 @@ class app: # create ID for the model from all its parameters self.get_ID() - print("Creating mesh", file=sys.stderr) + log.print_message("Creating mesh") # Create a mesh using one of two modes if self.mode == "planar": @@ -267,11 +263,11 @@ class app: self.make_stl_map() else: - self.error_exit("Mode not supported") + log.error_exit("Incorrect stl generation mode") plt.show() self.save_stl() - print(f"Saving model to ", self.stl_filename, file=sys.stderr) + log.print_message("Saving model to", self.stl_filename) def prepare_heightmap(self): '''Scale image values to get values from 0 to 255. @@ -281,27 +277,27 @@ class app: ''' if self.img.dtype != np.uint8: - print("Converting to uint8", file=sys.stderr) + log.print_message("Converting to uint8") self.img = self.img / np.max(self.img) * 255 self.img = self.img.astype(np.uint8) if self.mode == "planar": if self.height_base <= 0: - self.error_exit("Depth of plate height must be positive") + log.error_exit("Depth of plate height must be positive") if self.height_line + self.height_base <= 0: - self.error_exit("Line depth must be less than plate thickness") + log.error_exit("Line depth must be less than plate thickness") if self.mode == "curved": # Don't need to check curvature, check only heights if self.height_base <= 0 or self.height_line <= 0: - self.error_exit("Base and line height must both be positive") + log.error_exit("Base and line height must both be positive") if self.mode == "mapped": if self.height_line <= 0: - self.error_exit("Line height must be positive") + log.error_exit("Line height must be positive") if not exists(self.finger_model): - self.error_exit("Finger model file does not exist") + log.error_exit("Finger model file does not exist") self.height_base = 0 # TODO: curved height base could be done here? @@ -324,10 +320,10 @@ class app: # Truncate if necessary if (len(self.param_string) >= 80): self.param_string = self.param_string[:80] - print("Warning: Parameter string too long, truncating", file=sys.stderr) + log.print_message("Warning: Parameter string too long, truncating") # Overwrite stl header (which is only 80 bytes) - print("Writing info to stl header", file=sys.stderr) + log.print_message("Writing info to stl header") with open(self.stl_filename, "r+") as f: f.write(self.param_string) @@ -681,8 +677,7 @@ class app: # TODO: maybe use trimesh.update_vertices - - print("Mapping to finger") + log.print_message("Mapping to finger") # TODO: try to merge meshes? or stl files? # trimesh library? finger = trimesh.load(self.finger_model) @@ -715,7 +710,6 @@ class app: self.stl_model = trimesh.util.concatenate([finger, fingerprint]) - def save_stl(self): '''Save final mesh to stl file. ''' diff --git a/src/test.sh b/src/test.sh index 86be6f9..8ad7fbd 100644 --- a/src/test.sh +++ b/src/test.sh @@ -9,15 +9,15 @@ source .venv/bin/activate #----------------------------Configuration------------------------------# # place all image files to one folder -input_path=res/test/test-skript +input_path=res/test/Jenetrics/leva # !!!!!!!!!!!!!!!!!! # this is very important, run this on directories containing files with the same dpi -dpi=500 +dpi=600 # !!!!!!!!!!!!!!!!!! # recommend png, it's supported by opencv -format=jpg +format=png # name of configuration file config_file=conf/conf.json @@ -28,16 +28,18 @@ presets=("ridge") # generate stl files and set generation mode {"planar", "curved", "mapped"} generate_stl=true -generate_stl_mode="planar" +generate_stl_mode="curved" # in 1/10 of milimeters -height_line=2 -height_base=3 -curv_x=1.5 -curv_y=4 +height_line=2.5 +height_base=6 +curv_x=1.3 +curv_y=3 #----------------------------Application---------------------------# +# TODO: check if file will overwrite the original, print warning if that's the case + # function to apply filter to all files in directory apply_filter() { for in in ${file_arr[@]} @@ -46,14 +48,19 @@ apply_filter() { [[ -f "$in" ]] || continue # skip stl files and files with preset name in them - [[ "$in" == *_* ]] && continue + [[ "$in" == *"ridge"* ]] && continue [[ "$in" == *".stl" ]] && continue ((i++)) - echo -e "|\n|----------------------- File no. $i: $in ------------------------------|\n" - + echo -e "\n|----------------------- File no. $i: $in ------------------------------|\n" if $generate_stl; then + out="${in%%${match1}*}${match1}$format" + + if [[ "$in" == "$out" ]]; then + echo "SCRIPT: Changing filename to avoid overwrite" + out="${in%%${match1}*}_mod${match1}$format" + fi case "$generate_stl_mode" in "planar") python3 $exec_path $in $out $dpi -c $config_file $1 --stl p $height_line $height_base || break @@ -65,7 +72,7 @@ apply_filter() { python3 $exec_path $in $out $dpi -c $config_file $1 --stl m $height_line $height_base $fp_file|| break ;; *) - echo "Invalid stl generation mode" + echo "SCRIPT: Invalid stl generation mode" exit 1 ;; esac @@ -93,11 +100,11 @@ done # apply all given filter presets to all the discovered files for preset in ${presets[@]} do - echo -e "\n|----------------------- Filter "$preset" ------------------------------|\n|" + echo -e "\n|----------------------- Filter "$preset" ------------------------------|\n" j=0 apply_filter $preset $j done echo -e "\n|--------------------------- Done ----------------------------------|\n" -echo "Skipped $j files" -echo "Generated $i files" \ No newline at end of file +echo "SCRIPT: Skipped $j files" +echo "SCRIPT: Generated $i files"