From 8af2e88b357d396e1b4e751c5dfbebfd79bc94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20L=C3=A1n?= Date: Fri, 28 Apr 2023 19:36:00 +0200 Subject: [PATCH] Added functionality for finger mapping, adjusted mapping mode parameters. --- src/main.py | 174 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 65 deletions(-) diff --git a/src/main.py b/src/main.py index abf6ab6..c7b0153 100644 --- a/src/main.py +++ b/src/main.py @@ -7,7 +7,6 @@ import argparse as ap from os.path import exists import hashlib -import math # Libraries for image processing import numpy as np @@ -16,11 +15,14 @@ import cv2 as cv from stl import mesh import trimesh import trimesh.transformations as tmtra +import trimesh.remesh as tmrem # Import custom image filter library import filters as flt import config_parser as cp import log +import math + class fingerprint_app: '''Main class for the application. @@ -37,7 +39,8 @@ class fingerprint_app: # Parse configuration from json config file if self.args.config: self.config_file, self.preset_name = self.args.config - cp.parse_conf(self.preset_name, self.filters, self.params, self.config_file) + cp.parse_conf(self.preset_name, self.filters, + self.params, self.config_file) elif self.args.filters: filter_index = 0 @@ -58,10 +61,11 @@ class fingerprint_app: self.params[filter_index][key] = value 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]) + cp.save_preset(self.filters, self.params, + self.args.database[0]) else: log.print_message("No filters given, saving original image") @@ -76,18 +80,18 @@ class fingerprint_app: self.run_filtering() else: log.error_exit("Input file " + self.input_file + - " does not exist") + " does not exist") if self.args.stl: self.parse_stl() - + def parse_arguments(self): '''Parse arguments from command line using argparse library. ''' parser = ap.ArgumentParser(prog='main.py', description='Program for transforming a 2D image into 3D fingerprint.', - usage='%(prog)s [-h] [-m | --mirror | --no-mirror] [-p] input_file output_file dpi ([-c | --config config_file preset] | [filters ...]) [-s | --stl p height_line height_base | --stl c height_line curv_rate_x curv_rate_y | --stl m height_line]') + usage='%(prog)s [-h] [-m | --mirror | --no-mirror] input_file output_file dpi ([-c | --config config_file preset] | [filters ...]) [-s | --stl p height_line height_base | --stl c height_line curv_rate_x curv_rate_y | --stl m height_line]') # positional arguments parser.add_argument("input_file", type=str, help="input file path") @@ -99,7 +103,7 @@ class fingerprint_app: help="switch to mirror input image") # another boolean switch argument, this time with value, name of the new file and dimensions - # TODO: behaves absolutely randomly for some reason + # TODO: behaves absolutely randomly for some reason, fix it parser.add_argument('-s', "--stl", type=str, nargs='*', help="create stl model from processed image") @@ -135,7 +139,7 @@ class fingerprint_app: self.height_line = float(self.args.stl[1]) self.height_base = float(self.args.stl[2]) log.print_message("Base height:", self.height_base, - "mm, lines depth/height:", self.height_line, "mm") + "mm, lines depth/height:", self.height_line, "mm") elif self.args.stl[0] == 'c': self.mode = "curved" @@ -146,24 +150,28 @@ class fingerprint_app: self.height_base = 10 self.curv_rate_x = 2 self.curv_rate_y = 6 - log.print_message("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]) 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) + "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: + if len(self.args.stl) < 6: 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] + self.iter = int(self.args.stl[2]) + self.finger_x = float(self.args.stl[3]) + self.finger_y = float(self.args.stl[4]) + self.finger_z = float(self.args.stl[5]) else: log.error_exit("Unrecognized generation mode") @@ -219,13 +227,16 @@ class fingerprint_app: if len(self.filters) != 0: for i, filter_name in enumerate(self.filters): # Get filter class from filter.py, use the apply method - filter = getattr(flt, filter_name) + try: + filter = getattr(flt, filter_name) + except AttributeError: + log.error_exit("Filter " + filter_name + " not found") 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])) - + "=", str(self.params[i+1][param])) + filter.apply(self, self.params[i+1]) else: pass @@ -260,12 +271,8 @@ class fingerprint_app: self.make_stl_curved() elif self.mode == "mapped": - # TODO: find a more suitable finger model self.make_stl_map() - else: - log.error_exit("Incorrect stl generation mode") - plt.show() self.save_stl() log.print_message("Saving model to", self.stl_filename) @@ -297,8 +304,8 @@ class fingerprint_app: if self.mode == "mapped": if self.height_line <= 0: log.error_exit("Line height must be positive") - if not exists(self.finger_model): - log.error_exit("Finger model file does not exist") + if self.iter < 0: + log.error_exit("Number of iterations must be positive orr zero") self.height_base = 0 # TODO: curved height base could be done here? @@ -367,8 +374,11 @@ class fingerprint_app: param_list.append(str(self.curv_rate_y)) if self.mode == "mapped": - #TODO - pass + param_list.append(str(self.height_line)) + param_list.append(str(self.iter)) + param_list.append(str(self.finger_x)) + param_list.append(str(self.finger_y)) + param_list.append(str(self.finger_z)) if self.mode == "planar": param_list.append("P") @@ -384,7 +394,8 @@ class fingerprint_app: # fill the rest with the ending char to rewrite any leftover header # this is done for easier parsing of the header self.param_string = "\\".join(param_list) - self.param_string = self.param_string + "\n" * (80 - len(self.param_string)) + self.param_string = self.param_string + \ + "\n" * (80 - len(self.param_string)) # hash the param string to get unique ID, this will be put in filename and on the back of the model # not using built-in hash function because it's seed cannot be set to constant number @@ -435,7 +446,6 @@ class fingerprint_app: plt.close() - # TODO: maybe don't use nested for loops, use numpy? # TODO: this is very badly written, fix it # TODO: this does not always work, fix it # add the bottom array @@ -498,7 +508,6 @@ class fingerprint_app: vertices = [] faces = [] - # TODO: don't like this, could be done using numpy vectorisation? # Iterate over all vertices, create faces for i in range(self.height - 1): for j in range(self.width - 1): @@ -564,7 +573,10 @@ class fingerprint_app: self.create_stl_mesh(faces, vertices) def make_stl_curved(self): - '''Map fingerprint to finger model. + '''Compute curved surface. + Create mesh from meshgrid. + Create vertices from meshgrid, add depth values from image. + Create faces from vertices. Add vectors and faces to the model. ''' # TODO: this might be done in a better way, comment @@ -672,50 +684,82 @@ class fingerprint_app: def make_stl_map(self): '''Map fingerprint to a given finger model. - - Experimental, does not work very well... ''' - # TODO: maybe use trimesh.update_vertices - - log.print_message("Mapping to finger") - # TODO: try to merge meshes? or stl files? - # trimesh library? - finger = trimesh.load(self.finger_model) - # TODO: connect with curved generation - - # manually tried to allign two models and concatenated - fingerprint = trimesh.load('res/0-norm1_e5f52c0fe1.stl') - - angle = math.pi - dir = [0, 0, 1] - center = [0, 0, 0] - mat = tmtra.rotation_matrix(angle, dir, center) - finger.apply_transform(mat) - - angle = -3 / 4 * math.pi - dir = [1, 0, 0] - center = [0, 0, 0] - mat = tmtra.rotation_matrix(angle, dir, center) - finger.apply_transform(mat) - - - # TODO: random values that works for one finger model... - # TODO: this can later be modified to map finger to the core of the finger. - x = 2 + (self.width * 25.4 / self.dpi / 2) - y = 5 + (self.height * 25.4 / self.dpi / 2) - z = 20 - mat = tmtra.translation_matrix( - [x, y, z]) - finger.apply_transform(mat) - - self.stl_model = trimesh.util.concatenate([finger, fingerprint]) + # Conversion constants for mm and pixels + mm2px = self.dpi/25.4 + px2mm = 25.4/self.dpi + + # Finds the image pixel closest to finger vertice in 2D plane + def find_nearest(ver1, ver2, img): + searched_point = np.array([ver1, ver2]) + + min1 = math.floor(ver1*mm2px) + max1 = math.ceil(ver1*mm2px) + min2 = math.floor(ver2*mm2px) + max2 = math.ceil(ver2*mm2px) + min_dist_point = img[min2][min1] + + for i in range(min2, max2 - 1): + for j in range(min1, max1 - 1): + if np.linalg.norm(img[i][j] - searched_point) < min_dist_point: + min_dist_point = img[i][j] + return min_dist_point + + # Load the finger model + finger = trimesh.load("res/finger-mod.stl") + + # Implicitly translate it to match middle of the fingerprint + # Later this can be modified + x = (self.width * px2mm / 2) + self.finger_x + y = (self.height * px2mm / 2) + self.finger_y + z = self.finger_z + matrix = tmtra.translation_matrix([x, y, z]) + finger.apply_transform(matrix) + + # Subdivide the finger mesh to allow for more precision + vertices, faces = tmrem.subdivide_loop( + finger.vertices, finger.faces, iterations=self.iter) + + # For logging progress + c = 0 + u = 0 + for k, vertice in enumerate(vertices): + # Skip vertices under plane xy + # also skip vertices under the fingerprint image, + # they are all unused + if vertice[2] < 0 or vertice[1] > self.height * px2mm: + u += 1 + continue + + # This is the easiest way to avoid indexing errors + # Those errors are caused by vertices outside of the image + # When this occurs, input parameters need to be adjusted + try: + # Find the closest point in the image + # To the 2D image projection of vertice, add its value + point = find_nearest(vertice[0], vertice[1], self.img) + except IndexError: + log.error_exit("Fingerprint image is outside of the finger model") + + vertices[k][2] += point + + # Prints out generation progress + if k % 1000 == 0: + percentage = round(k/len(vertices) * 100, 2) + if percentage > c: + log.print_message("Carving finger: " + str(c) + "%") + c += 10 + + self.stl_model = trimesh.Trimesh(vertices, faces) + log.print_message("Carving finger finished") + print("Unused vertices: " + str(u)) def save_stl(self): '''Save final mesh to stl file. ''' - # create output file name, save it and write header with file info + # Create output file name, save it and write header with file info self.stl_filename = self.output_file.split( ".")[0] + "_" + self.id + ".stl" if (self.mode == "mapped"):