import cv2 import numpy as np import trimesh from shapely.geometry import Polygon import tkinter as tk from tkinter import filedialog, messagebox from PIL import Image, ImageTk import os class KeyForge3DApp: def __init__(self, root): self.root = root self.root.title("KeyForge3D - Key Shape Extractor and 3D Model Generator") self.root.geometry("600x400") # Variables self.image_path = None self.scale_factor = 0.1 # Default scale: 1 pixel = 0.1 mm (adjust with a reference object if needed) self.key_thickness = 2.0 # Thickness of the key in mm self.num_cuts = 5 # Number of bitting cuts (adjust based on key type) self.cut_depth_increment = 0.33 # Depth increment per bitting value in mm (e.g., Schlage standard) # GUI Elements self.label = tk.Label(root, text="KeyForge3D: Extract and 3D Print Keys", font=("Arial", 16)) self.label.pack(pady=10) self.upload_button = tk.Button(root, text="Upload Key Image", command=self.upload_image) self.upload_button.pack(pady=5) self.image_label = tk.Label(root) self.image_label.pack(pady=5) self.process_button = tk.Button(root, text="Process Key and Generate 3D Model", command=self.process_key, state=tk.DISABLED) self.process_button.pack(pady=5) self.result_label = tk.Label(root, text="", font=("Arial", 12)) self.result_label.pack(pady=5) def upload_image(self): """Allow the user to upload an image of a key.""" self.image_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.jpg *.jpeg *.png")]) if self.image_path: # Display the uploaded image img = Image.open(self.image_path) img = img.resize((300, 150), Image.Resampling.LANCZOS) # Resize for display img_tk = ImageTk.PhotoImage(img) self.image_label.config(image=img_tk) self.image_label.image = img_tk # Keep a reference to avoid garbage collection self.process_button.config(state=tk.NORMAL) self.result_label.config(text="Image uploaded. Click 'Process Key' to generate the 3D model.") def process_key(self): """Process the key image, extract the shape, and generate a 3D model.""" if not self.image_path: messagebox.showerror("Error", "Please upload an image first.") return try: # Load the image image = cv2.imread(self.image_path) if image is None: raise ValueError("Could not load the image.") gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Apply Gaussian blur and edge detection blurred = cv2.GaussianBlur(gray, (5, 5), 0) edges = cv2.Canny(blurred, 50, 150) # Find contours contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # Filter contours to find the key (long, thin shape) key_contour = None for contour in contours: perimeter = cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True) x, y, w, h = cv2.boundingRect(contour) aspect_ratio = w / float(h) if 2 < aspect_ratio < 5 and w > 100: # Adjust these values based on your image key_contour = contour break if key_contour is None: raise ValueError("Could not detect a key in the image.") # Extract the key region x, y, w, h = cv2.boundingRect(key_contour) key_region = gray[y:y+h, x:x+w] # Convert contour to a 2D polygon points = key_contour.reshape(-1, 2) * self.scale_factor key_polygon = Polygon(points) # Extrude the polygon to create a 3D model key_mesh = trimesh.creation.extrude_polygon(key_polygon, height=self.key_thickness) # Analyze the bitting blade = key_region[h//2:h, :] # Focus on the lower half (blade) height, width = blade.shape segment_width = width // self.num_cuts bitting = [] for i in range(self.num_cuts): segment = blade[:, i * segment_width:(i + 1) * segment_width] # Find the highest point (shallowest cut) in the segment cut_depth = np.argmax(segment, axis=0).mean() # Scale the depth to real-world dimensions depth_value = (cut_depth / height) * self.cut_depth_increment * 9 bitting.append(depth_value) # Apply bitting cuts to the 3D model for i, depth in enumerate(bitting): cut_x = (i * segment_width * self.scale_factor) + (segment_width * self.scale_factor / 2) cut_y = 0 # Adjust based on blade position cut_width = segment_width * self.scale_factor cut_height = depth # Create a box for the cut and subtract it from the key mesh cut_box = trimesh.creation.box( extents=[cut_width, cut_height, self.key_thickness + 1], transform=trimesh.transformations.translation_matrix([cut_x, cut_y, 0]) ) key_mesh = key_mesh.difference(cut_box) # Export the 3D model as STL output_path = "key_model.stl" key_mesh.export(output_path) # Display the results bitting_code = [int(d / self.cut_depth_increment) for d in bitting] self.result_label.config( text=f"Success! Bitting Code: {bitting_code}\n3D Model saved as '{output_path}'" ) messagebox.showinfo("Success", f"3D model generated and saved as '{output_path}'.") except Exception as e: messagebox.showerror("Error", f"Failed to process the key: {str(e)}") self.result_label.config(text="Error processing the key. See error message.") if __name__ == "__main__": root = tk.Tk() app = KeyForge3DApp(root) root.mainloop()