diff --git a/src/main.py b/src/main.py index 6ff1203..623794b 100644 --- a/src/main.py +++ b/src/main.py @@ -55,10 +55,20 @@ class app: self.params[i][key] = value self.parse_params(self.params[i]) - if self.args.stl_file: + if self.args.stl_file and len(self.args.stl_file) == 3: + self.stl_file = self.args.stl_file[0] + self.height_line = float(self.args.stl_file[1]) + self.height_base = float(self.args.stl_file[2]) + self.mode = "2d" + + elif self.args.stl_file and len(self.args.stl_file) == 2: self.stl_file = self.args.stl_file[0] - self.depth_total = float(self.args.stl_file[1]) - self.depth_line = float(self.args.stl_file[2]) + self.height_line = float(self.args.stl_file[1]) + self.mode = "3d" + + else: + print("No STL file given, saving image only") + exit(1) self.input_file = self.args.input_file self.output_file = self.args.output_file @@ -78,7 +88,6 @@ class app: self.width = self.img.shape[1] self.height = self.img.shape[0] self.print_size(self.img.shape) - print(self.dpi) fig = plt.figure(figsize=(self.width/self.dpi, self.height/self.dpi), frameon=False, dpi=self.dpi) @@ -94,7 +103,7 @@ class app: self.save_image(fig, ax) plt.close() if self.args.stl_file: - self.make_lithophane() + self.make_model() def parse_params(self, params): ''' Parse parameters of filters. @@ -137,22 +146,20 @@ class app: 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 ...]) [-s stl_file | --stl_file stl_file depth_total depth_line]') + ([-c config_file preset | --config config_file preset] | [filters ...]) \ + [-s stl_file | --stl_file stl_file depth_total depth_line]') # positional arguments - parser.add_argument("input_file", type=str, - help="location with input file") - parser.add_argument("output_file", type=str, - help="output file location") + parser.add_argument("input_file", type=str, help="location with input file") + parser.add_argument("output_file", type=str, help="output file location") parser.add_argument("dpi", type=int, help="scanner dpi") # boolean switch argument - parser.add_argument('-m', "--mirror", help="mirror input image", - type=bool, action=ap.BooleanOptionalAction) + parser.add_argument('-m', "--mirror", help="mirror input image", type=bool, action=ap.BooleanOptionalAction) - # another boolean switch argument, this time with value, name of the new file - parser.add_argument('-s', '--stl_file', type=str, nargs=3, - help="make stl model from processed image", required=False) + # another boolean switch argument, this time with value, name of the new file and dimensions + parser.add_argument('-s', '--stl_file', type=str, nargs='*', + help="make planar model from processed image", required=False) # file with configuration containing presets, new preset name # pair argument - give both or none @@ -195,10 +202,6 @@ class app: filter = self.filter_factory(filter_name) filter.apply(self, self.params[i+1]) - def print_size(self, size): - 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. Colormap set to grayscale to avoid color mismatch. @@ -208,24 +211,34 @@ class app: ax.imshow(self.img, cmap="gray") fig.savefig(fname=self.output_file, dpi='figure') - def make_lithophane(self): + def print_size(self, size): + print("Image of height: " + str(size[0]) + + " px and width: " + str(size[1]) + " px", file=sys.stderr) + + def make_model(self): '''After processing image, make a lithophane from it. ''' - print("Making meshgrid", file=sys.stderr) - self.make_meshgrid() - print("Converting to stl format", file=sys.stderr) - self.make_stl_planar() - #print("Mapping to 3D finger", file=sys.stderr) - #self.map_image_to_3d() - plt.show() - print(f"Saving lithophane to ", self.stl_file, file=sys.stderr) - self.save_stl() - - def make_meshgrid(self): + print("Making heighthmap", file=sys.stderr) + self.prepare_heightmap() + + if self.mode == "2d": + print("Converting to stl format", file=sys.stderr) + self.make_stl_planar() + plt.show() + print(f"Saving lithophane to ", self.stl_file, file=sys.stderr) + self.save_stl_2d() + elif self.mode == "3d": + self.map_image_to_3d() + plt.show() + self.save_stl_3d() + else: + print("Mode not supported", file=sys.stderr) + exit(1) + + def prepare_heightmap(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 values to get usable depth values. ''' if self.img.dtype == np.float32 or self.img.dtype == np.float64: @@ -233,29 +246,43 @@ class app: self.img = self.img * 255 self.img = self.img.astype(np.uint8) - print("Total depth:", self.depth_total, "mm, line depth/height:", self.depth_line, "mm") - depth_base = self.depth_total - self.depth_line + if self.mode == "2d": + if self.height_base <= 0: + print("Depth of plate height must be positive", file=sys.stderr) + exit(1) - # Make depth map from image - if self.depth_line < 0: - self.img = (depth_base + (1 - self.img/255) * self.depth_line) - else: - self.img = (depth_base + (1 - self.img/255) * self.depth_line) - - # Create meshgrid for 3D model - # This sets the scale of stl model and number of subdivisions / triangles - x = np.linspace(0, self.width * 25.4 / self.dpi, self.width) - y = np.linspace(0, self.height * 25.4 / self.dpi, self.height) + if self.height_line + self.height_base <= 0: + print("Line depth must be less than plate thickness", file=sys.stderr) + exit(1) - self.meshgrid = np.meshgrid(x, y) + print("Base height:", self.height_base, + "mm, lines depth/height:", self.height_line, "mm") + + # Transform image values to get a heightmap + if self.height_line < 0: + self.img = (self.height_base + (1 - self.img/255) + * self.height_line) + else: + self.img = (self.height_base + (1 - self.img/255) + * self.height_line) + + if self.mode == "3d": + #TODO add some checks and print info + pass + + def add_faces(self, faces, c): + # Function to add faces to the list + faces.append([c, c + 1, c + 2]) + faces.append([c + 1, c + 3, c + 2]) + return c + 4 def make_stl_planar(self): - ''' Create mesh from image. + ''' Create mesh from meshgrid. Create vertices from meshgrid, add depth values from image. - Create faces from vertices. Add veectors to the model. + Create faces from vertices. Add vectors and faces to the model. From wikipedia.org/wiki/STL_(file_format): - ascii stl format consists of repeating struictures: + ascii stl format consists of repeating structures: facet normal ni nj nk # normal vector outer loop @@ -264,11 +291,16 @@ class app: vertex v3x v3y v3z # vertex 3 endloop endfacet - ''' + # This sets the size of stl model and number of subdivisions / triangles + x = np.linspace(0, self.width * 25.4 / self.dpi, self.width) + y = np.linspace(0, self.height * 25.4 / self.dpi, self.height) + + self.meshgrid_2d = np.meshgrid(x, y) + # Add the image matrix to the 2D meshgrid and create 1D array of 3D points - vertex_arr = np.vstack(list(map(np.ravel, self.meshgrid))).T + vertex_arr = np.vstack(list(map(np.ravel, self.meshgrid_2d))).T z = (self.img / 10).reshape(-1, 1) vertex_arr = np.concatenate((vertex_arr, z), axis=1) @@ -279,12 +311,6 @@ class app: vertices = [] faces = [] - # Function to add faces to the list - def add_faces(c): - faces.append([c, c + 1, c + 2]) - faces.append([c + 1, c + 3, c + 2]) - return c + 4 - # Iterate over all vertices, create faces for i in range(self.height - 1): for j in range(self.width - 1): @@ -294,7 +320,7 @@ class app: vertices.append([vertex_arr[i+1][j]]) vertices.append([vertex_arr[i+1][j+1]]) - count = add_faces(count) + count = self.add_faces(faces, count) # Add faces for the backside of the lithophane null_arr = np.copy(vertex_arr) @@ -311,7 +337,7 @@ class app: vertices.append([null_arr[i][j+1]]) vertices.append([null_arr[i+1][j+1]]) - count = add_faces(count) + count = self.add_faces(faces, count) # Horizontal side faces for j in range(self.height - 1): @@ -320,7 +346,7 @@ class app: vertices.append([null_arr[j][0]]) vertices.append([null_arr[j+1][0]]) - count = add_faces(count) + count = self.add_faces(faces, count) max = self.width - 1 @@ -329,7 +355,7 @@ class app: vertices.append([null_arr[j+1][max]]) vertices.append([null_arr[j][max]]) - count = add_faces(count) + count = self.add_faces(faces, count) # Vertical side faces for j in range(self.width - 1): @@ -338,7 +364,7 @@ class app: vertices.append([null_arr[0][j+1]]) vertices.append([null_arr[0][j]]) - count = add_faces(count) + count = self.add_faces(faces, count) max = self.height - 1 @@ -347,38 +373,62 @@ class app: vertices.append([null_arr[max][j]]) vertices.append([null_arr[max][j+1]]) - count = add_faces(count) + count = self.add_faces(faces, count) # Convert to numpy arrays faces = np.array(faces) vertices = np.array(vertices) # Create the mesh - vertices.shape (no_faces, 3, 3) - self.stl_mesh = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) + self.stl_mesh_2d = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) for i, face in enumerate(faces): for j in range(3): - self.stl_mesh.vectors[i][j] = vertices[face[j], :] + self.stl_mesh_2d.vectors[i][j] = vertices[face[j], :] - def save_stl(self): + def save_stl_2d(self): ''' Save final mesh to stl file. ''' - self.stl_mesh.save(self.stl_file) + self.stl_mesh_2d.save(self.stl_file) def map_image_to_3d(self): ''' Map fingerprint to finger model. ''' - x = np.linspace(0, 25.4, self.dpi) - y = np.linspace(0, 25.4, self.dpi) - z1 = np.linspace(0, 10, self.dpi) - z2 = np.linspace(10, 0, self.dpi) - z = np.concatenate((z1, z2)) - tmp = np.meshgrid(x, y) - self.mesh_finger = np.meshgrid(tmp,z) - print(len(self.mesh_finger)) - #self.finger_base = mesh.Mesh( - #np.zeros(, dtype=mesh.Mesh.dtype)) + x = np.linspace(0, self.width * 25.4 / self.dpi, self.width) + y = np.linspace(0, self.height * 25.4 / self.dpi, self.height) + + z1 = np.logspace(0, 10, int(np.ceil(self.width / 2)), base=0.7) + z2 = np.logspace(10, 0, int(np.floor(self.width / 2)), base=0.7) + ztemp = 5*np.concatenate((z1, z2)) + + z = np.array([]) + for i in range(self.height): + z = np.concatenate((z, ztemp * pow(np.log(i+2), -1))) + z = z.reshape(-1, 1) + + self.meshgrid_3d = np.meshgrid(x, y) + + vertex_arr = np.vstack(list(map(np.ravel, self.meshgrid_3d))).T + vertex_arr = np.concatenate((vertex_arr, z), axis=1) + vertex_arr = vertex_arr.reshape(self.height, self.width, 3) + + count = 0 + vertices = [] + faces = [] + + # Iterate over all vertices, create faces + for i in range(self.height - 1): + for j in range(self.width - 1): + + vertices.append([vertex_arr[i][j]]) + vertices.append([vertex_arr[i][j+1]]) + vertices.append([vertex_arr[i+1][j]]) + vertices.append([vertex_arr[i+1][j+1]]) + + count = self.add_faces(faces, count) + + #self.finger_base = mesh.Mesh(np.zeros(, dtype=mesh.Mesh.dtype)) # linear projection # extrude lines in 1 direction @@ -387,5 +437,20 @@ class app: # normal projection # extrude lines in the direction of normals of given finger model + # Convert to numpy arrays + faces = np.array(faces) + vertices = np.array(vertices) + + # Create the mesh - vertices.shape (no_faces, 3, 3) + self.mesh_finger = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) + for i, face in enumerate(faces): + for j in range(3): + self.mesh_finger.vectors[i][j] = vertices[face[j], :] + + def save_stl_3d(self): + ''' Save final mesh to stl file. + ''' + + self.mesh_finger.save(self.stl_file) image = app()