From 234f2cb79a2b9a355ee013493ca5c0dece5a5606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rostislav=20L=C3=A1n?= Date: Tue, 28 Feb 2023 18:02:17 +0100 Subject: [PATCH] Added text engraving to the bottom, removed some redundant code. --- src/main.py | 322 ++++++++++++++++++++++++++++------------------------ 1 file changed, 176 insertions(+), 146 deletions(-) diff --git a/src/main.py b/src/main.py index 3b3797f..8cfd886 100644 --- a/src/main.py +++ b/src/main.py @@ -205,15 +205,12 @@ class app: self.input_file, cv.IMREAD_GRAYSCALE).astype(np.uint8) self.height, self.width = self.img.shape + # gets empty figure and ax with dimensions of input image + fig, ax = self.get_empty_figure() + print("Height: " + str(self.height) + " px and width: " + str(self.width) + " px", file=sys.stderr) - size = (self.width/self.dpi, self.height/self.dpi) - fig = plt.figure(figsize=size, frameon=False, dpi=self.dpi) - - ax = plt.Axes(fig, [0., 0., 1., 1.]) - ax.set_axis_off() - fig.add_axes(ax) - + if self.mirror is True: self.mirror_image() @@ -222,6 +219,16 @@ class app: self.save_image(fig, ax) plt.close() + def get_empty_figure(self): + '''Return empty figure with one ax of dimensions of input image. + ''' + size = (self.width/self.dpi, self.height/self.dpi) + fig = plt.figure(figsize=size, frameon=False, dpi=self.dpi) + ax = plt.Axes(fig, [0., 0., 1., 1.]) + ax.set_axis_off() + fig.add_axes(ax) + return fig, ax + def mirror_image(self): '''Mirror image using opencv, should be used if we want a positive model. ''' @@ -259,6 +266,7 @@ class app: ''' self.prepare_heightmap() + self.get_ID() if self.mode == "2d": self.make_stl_planar() @@ -270,12 +278,13 @@ class app: self.error_exit("Mode not supported") plt.show() - print(f"Saving model to ", self.stl_file, file=sys.stderr) self.save_stl() + print(f"Saving model to ", self.stl_file, file=sys.stderr) def prepare_heightmap(self): '''Modify image values to get usable height/depth values. Check validity of dimension parameters. + Prepare meshgrid. ''' # TODO: redo, too complicated, add extra params, redo checks @@ -303,12 +312,84 @@ class app: # Transform image values to get a heightmap self.img = (1 - self.img/255) * self.height_line + # 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 = np.meshgrid(x, y) + + def get_ID(self): + '''Get unique ID for the model. + Consists of pair input_file + preset_name. + ''' + # TODO: somehow compress this to fit it onto the model, maybe zlib + self.id = self.input_file.split( + "/")[-1].split(".")[0] + "_" + self.preset_name + print(self.id) + def append_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 engrave_text(self, bottom_vert_arr): + '''Engrave text on the back of the model. + Create an empty image, fill it with color and draw text on it. + ''' + + fig, ax = self.get_empty_figure() + + # paint the background black + ax.plot([0, 1], [0, 1], c="black", lw=self.width) + + # extract text from filename + text = self.stl_file.split("/")[-1].split(".")[0] + self.id + fontsize = 20 + + # create text object, paint it white + t = ax.text(0.5, 0.5, text, ha="center", va="center", + fontsize=30, c="white", rotation=90, wrap=True, clip_on=True) + + # adjust fontsize to fit text in the image + # matplotlib does not support multiline text, wrapping is broken + rend = fig.canvas.get_renderer() + while (t.get_window_extent(rend).width > self.width or t.get_window_extent(rend).height > self.height): + fontsize -= 0.3 + t.set_fontsize(fontsize) + + # update figure, read pixels and reshape to 3d array + fig.canvas.draw() + data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) + data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + + # scale inscription layer to suitable height + data = (data/255)/10 + + plt.close() + + # TODO: maybe don't use nested for loops, use numpy? + for i in range(self.height): + for j in range(self.width): + bottom_vert_arr[i][j][2] = data[i][j][0] + + return bottom_vert_arr + + def create_stl_mesh(self, faces, vertices): + '''Create mesh from faces and vertices. + ''' + + # Convert lists to numpy arrays + faces = np.array(faces) + vertices = np.array(vertices) + + # Create the mesh - vertices.shape (no_faces, 3, 3) + self.stl_model = mesh.Mesh( + np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) + for i, face in enumerate(faces): + for j in range(3): + self.stl_model.vectors[i][j] = vertices[face[j], :] + def make_stl_planar(self): '''Create mesh from meshgrid. Create vertices from meshgrid, add depth values from image. @@ -326,123 +407,87 @@ class app: 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 pointsd - vertex_arr = np.vstack(list(map(np.ravel, self.meshgrid_2d))).T + # Add the image matrix to the 2D meshgrid and create 1D array of 3D points + top_vert_arr = np.vstack(list(map(np.ravel, self.meshgrid))).T z = (self.img / 10).reshape(-1, 1) - vertex_arr = np.concatenate((vertex_arr, z), axis=1) + top_vert_arr = np.concatenate((top_vert_arr, z), axis=1) # Convert 1D array back to matrix of 3D points - vertex_arr = vertex_arr.reshape(self.height, self.width, 3) + top_vert_arr = top_vert_arr.reshape(self.height, self.width, 3) count = 0 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): - 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]]) + vertices.append([top_vert_arr[i][j]]) + vertices.append([top_vert_arr[i][j+1]]) + vertices.append([top_vert_arr[i+1][j]]) + vertices.append([top_vert_arr[i+1][j+1]]) count = self.append_faces(faces, count) - # Add faces for the backside of the lithophane - null_arr = np.copy(vertex_arr) - for i in range(self.height): - for j in range(self.width): - null_arr[i][j][2] = 0 + # Prepare image with plotted text for the backside of the lithophane + bottom_vert_arr = np.copy(top_vert_arr) + self.engrave_text(bottom_vert_arr) # Back side faces for i in range(self.height - 1): for j in range(self.width - 1): - vertices.append([null_arr[i][j]]) - vertices.append([null_arr[i+1][j]]) - vertices.append([null_arr[i][j+1]]) - vertices.append([null_arr[i+1][j+1]]) + vertices.append([bottom_vert_arr[i][j]]) + vertices.append([bottom_vert_arr[i+1][j]]) + vertices.append([bottom_vert_arr[i][j+1]]) + vertices.append([bottom_vert_arr[i+1][j+1]]) count = self.append_faces(faces, count) # Horizontal side faces for i in range(self.height - 1): - vertices.append([vertex_arr[i][0]]) - vertices.append([vertex_arr[i+1][0]]) - vertices.append([null_arr[i][0]]) - vertices.append([null_arr[i+1][0]]) + vertices.append([top_vert_arr[i][0]]) + vertices.append([top_vert_arr[i+1][0]]) + vertices.append([bottom_vert_arr[i][0]]) + vertices.append([bottom_vert_arr[i+1][0]]) count = self.append_faces(faces, count) max = self.width - 1 - vertices.append([vertex_arr[i+1][max]]) - vertices.append([vertex_arr[i][max]]) - vertices.append([null_arr[i+1][max]]) - vertices.append([null_arr[i][max]]) + vertices.append([top_vert_arr[i+1][max]]) + vertices.append([top_vert_arr[i][max]]) + vertices.append([bottom_vert_arr[i+1][max]]) + vertices.append([bottom_vert_arr[i][max]]) count = self.append_faces(faces, count) # Vertical side faces for j in range(self.width - 1): - vertices.append([vertex_arr[0][j+1]]) - vertices.append([vertex_arr[0][j]]) - vertices.append([null_arr[0][j+1]]) - vertices.append([null_arr[0][j]]) + vertices.append([top_vert_arr[0][j+1]]) + vertices.append([top_vert_arr[0][j]]) + vertices.append([bottom_vert_arr[0][j+1]]) + vertices.append([bottom_vert_arr[0][j]]) count = self.append_faces(faces, count) max = self.height - 1 - vertices.append([vertex_arr[max][j]]) - vertices.append([vertex_arr[max][j+1]]) - vertices.append([null_arr[max][j]]) - vertices.append([null_arr[max][j+1]]) + vertices.append([top_vert_arr[max][j]]) + vertices.append([top_vert_arr[max][j+1]]) + vertices.append([bottom_vert_arr[max][j]]) + vertices.append([bottom_vert_arr[max][j+1]]) count = self.append_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_lithophane = mesh.Mesh( - np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype)) - for i, face in enumerate(faces): - for j in range(3): - self.stl_lithophane.vectors[i][j] = vertices[face[j], :] + self.create_stl_mesh(faces, vertices) def make_stl_curved(self): '''Map fingerprint to finger model. ''' - # TODO: if this is the same as 2D, move to heightmap to reduce duplicate code - 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_3d = np.meshgrid(x, y) - - # Method 1 - logspace and logarithmic curve - '''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 + 25*(((i+50)/20)**(-1/2)))) - z = z.reshape(-1, 1) - - self.img = (self.img / 10).reshape(-1, 1) - z += self.img''' - - # Method 2 - 2 ellipses z = np.array([]) for x in range(self.width): z = np.append(z, np.sqrt(1 - (2*x/self.width - 1)**2) @@ -452,134 +497,119 @@ class app: new = np.sqrt((1 - ((self.height - y)/self.height)**2) * (self.curv_rate_y**2)) z[y] = z[y] + new - - # TODO: clip responsivelly - bottom = z[0][math.floor(self.width/2)] - #top = self.curv_rate_x**2 + self.curv_rate_y - #np.clip(z, bottom, top, out=z) + + # TODO: clip responsivelly to save material used to print the model + #bottom = z[0][math.floor(self.width/2)] z = z.reshape(-1, 1) self.img = (self.img / 10).reshape(-1, 1) z += self.img - 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) + top_vert_arr = np.vstack(list(map(np.ravel, self.meshgrid))).T + top_vert_arr = np.concatenate((top_vert_arr, z), axis=1) + top_vert_arr = top_vert_arr.reshape(self.height, self.width, 3) count = 0 vertices = [] faces = [] - min_point = 0 - for i in range(self.height - 1): - if vertex_arr[i][0][2] <= bottom: - min_point = i + + #min_point = 0 + #for i in range(self.height - 1): + # if top_vert_arr[i][0][2] <= bottom: + # min_point = i # Add faces for the backside of the lithophane - vec_side = (vertex_arr[self.height-1][0][2] - - vertex_arr[min_point][0][2]) / (self.height - min_point) - null_arr = np.copy(vertex_arr) - for i in range(self.height): - for j in range(self.width): - null_arr[i][j][2] = 0 - #null_arr[i][j][2] = bottom + vec_side * i - # for smaller mesh + #vec_side = (top_vert_arr[self.height-1][0][2] - + # top_vert_arr[min_point][0][2]) / (self.height - min_point) + bottom_vert_arr = np.copy(top_vert_arr) + self.engrave_text(bottom_vert_arr) + + # TODO: code bellow is duplicate of the code in planar generation + # if not changed move to a separate function and simplify # Iterate over all vertices, create faces for i in range(self.height - 1): for j in range(self.width - 1): - if (vertex_arr[i][j][2] <= null_arr[i][j][2] - or vertex_arr[i+1][j][2] <= null_arr[i+1][j][2] - or vertex_arr[i][j+1][2] <= null_arr[i][j+1][2] - or vertex_arr[i+1][j+1][2] <= null_arr[i+1][j+1][2]): + if (top_vert_arr[i][j][2] <= bottom_vert_arr[i][j][2] + or top_vert_arr[i+1][j][2] <= bottom_vert_arr[i+1][j][2] + or top_vert_arr[i][j+1][2] <= bottom_vert_arr[i][j+1][2] + or top_vert_arr[i+1][j+1][2] <= bottom_vert_arr[i+1][j+1][2]): continue - 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]]) + vertices.append([top_vert_arr[i][j]]) + vertices.append([top_vert_arr[i][j+1]]) + vertices.append([top_vert_arr[i+1][j]]) + vertices.append([top_vert_arr[i+1][j+1]]) count = self.append_faces(faces, count) # Rotated back side faces for i in range(self.height - 1): for j in range(self.width - 1): - if (vertex_arr[i][j][2] <= null_arr[i][j][2]): + if (top_vert_arr[i][j][2] <= bottom_vert_arr[i][j][2]): continue - vertices.append([null_arr[i][j]]) - vertices.append([null_arr[i+1][j]]) - vertices.append([null_arr[i][j+1]]) - vertices.append([null_arr[i+1][j+1]]) + vertices.append([bottom_vert_arr[i][j]]) + vertices.append([bottom_vert_arr[i+1][j]]) + vertices.append([bottom_vert_arr[i][j+1]]) + vertices.append([bottom_vert_arr[i+1][j+1]]) count = self.append_faces(faces, count) # Horizontal side faces for i in range(self.height - 1): # right - #if (vertex_arr[i][0][2] < null_arr[i][0][2]): + #if (top_vert_arr[i][0][2] < bottom_vert_arr[i][0][2]): # continue - vertices.append([vertex_arr[i][0]]) - vertices.append([vertex_arr[i+1][0]]) - vertices.append([null_arr[i][0]]) - vertices.append([null_arr[i+1][0]]) + vertices.append([top_vert_arr[i][0]]) + vertices.append([top_vert_arr[i+1][0]]) + vertices.append([bottom_vert_arr[i][0]]) + vertices.append([bottom_vert_arr[i+1][0]]) count = self.append_faces(faces, count) for i in range(self.height - 1): # left max = self.width - 1 - #if (vertex_arr[i][max][2] < null_arr[i][max][2]): + #if (top_vert_arr[i][max][2] < bottom_vert_arr[i][max][2]): # continue - vertices.append([vertex_arr[i+1][max]]) - vertices.append([vertex_arr[i][max]]) - vertices.append([null_arr[i+1][max]]) - vertices.append([null_arr[i][max]]) + vertices.append([top_vert_arr[i+1][max]]) + vertices.append([top_vert_arr[i][max]]) + vertices.append([bottom_vert_arr[i+1][max]]) + vertices.append([bottom_vert_arr[i][max]]) count = self.append_faces(faces, count) # Vertical side faces for j in range(self.width - 1): # top - #if (vertex_arr[0][j][2] < null_arr[0][j][2]): + #if (top_vert_arr[0][j][2] < bottom_vert_arr[0][j][2]): # continue - vertices.append([vertex_arr[0][j+1]]) - vertices.append([vertex_arr[0][j]]) - vertices.append([null_arr[0][j+1]]) - vertices.append([null_arr[0][j]]) + vertices.append([top_vert_arr[0][j+1]]) + vertices.append([top_vert_arr[0][j]]) + vertices.append([bottom_vert_arr[0][j+1]]) + vertices.append([bottom_vert_arr[0][j]]) count = self.append_faces(faces, count) for j in range(self.width - 1): # bottom max = self.height - 1 - #if (vertex_arr[max][j][2] < null_arr[max][j][2]): + #if (top_vert_arr[max][j][2] < bottom_vert_arr[max][j][2]): # continue - vertices.append([vertex_arr[max][j]]) - vertices.append([vertex_arr[max][j+1]]) - vertices.append([null_arr[max][j]]) - vertices.append([null_arr[max][j+1]]) + vertices.append([top_vert_arr[max][j]]) + vertices.append([top_vert_arr[max][j+1]]) + vertices.append([bottom_vert_arr[max][j]]) + vertices.append([bottom_vert_arr[max][j+1]]) count = self.append_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.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], :] - - # print(self.mesh_finger.normals) + self.create_stl_mesh(faces, vertices) def save_stl(self): '''Save final mesh to stl file. ''' # TODO: add a hash function to create filename specific to input image and preset - if self.mode == "3d": - self.mesh_finger.save(self.stl_file) - else: - self.stl_lithophane.save(self.stl_file) + self.stl_file = self.stl_file.split(".")[0] + "_" + self.id + "." + self.stl_file.split(".")[1] + self.stl_model.save(self.stl_file) # run the application