|
|
|
@ -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"):
|
|
|
|
|