import numpy as np from scipy import ndimage from sklearn.cluster import MiniBatchKMeans def vectorized_edge_preserving_quantization( voxel_matrix, lego_colors, n_initial_clusters=50 ): """ Vectorized version of edge-preserving color quantization Parameters: voxel_matrix: numpy array of shape (x, y, z, 3) containing RGB values lego_colors: list of [R, G, B] values for target LEGO colors n_initial_clusters: number of initial color clusters before mapping to LEGO colors """ lego_colors = np.array(lego_colors) shape = voxel_matrix.shape # Reshape to 2D array of pixels pixels = voxel_matrix.reshape(-1, 3) # Step 1: Initial color clustering using K-means kmeans = MiniBatchKMeans( n_clusters=n_initial_clusters, batch_size=1000, random_state=42 ) labels = kmeans.fit_predict(pixels) cluster_centers = kmeans.cluster_centers_ # Step 2: Create 3D gradient magnitude gradients = np.zeros(shape[:3]) # Compute gradients along each axis for axis in range(3): # Forward difference forward = np.roll(voxel_matrix, -1, axis=axis) # Compute color differences diff = np.sqrt(np.sum((forward - voxel_matrix) ** 2, axis=-1)) # Set boundary differences to 0 slice_idx = [slice(None)] * 3 slice_idx[axis] = -1 diff[tuple(slice_idx)] = 0 gradients += diff # Step 3: Segment using watershed algorithm # Reshape labels back to 3D labels_3d = labels.reshape(shape[:3]) # Find local minima in gradient magnitude markers = ndimage.label(ndimage.minimum_filter(gradients, size=3) == gradients)[0] # Apply watershed segmentation segments = ndimage.watershed_ift(gradients.astype(np.uint8), markers) # Step 4: Map segments to LEGO colors # Get mean color for each segment segment_colors = ndimage.mean( voxel_matrix.reshape(-1, 3), labels=segments.ravel(), index=np.arange(segments.max() + 1), ) # Find nearest LEGO color for each segment color§ def find_nearest_lego_colors(colors): # Reshape inputs for broadcasting colors = colors[:, np.newaxis, :] lego_colors_r = lego_colors[np.newaxis, :, :] # Compute distances to all LEGO colors at once distances = np.sqrt(np.sum((colors - lego_colors_r) ** 2, axis=2)) # Find index of minimum distance for each color nearest_indices = np.argmin(distances, axis=1) return lego_colors[nearest_indices] segment_lego_colors = find_nearest_lego_colors(segment_colors) # Create output array result = np.zeros_like(voxel_matrix) # Map segments to final colors for i, color in enumerate(segment_lego_colors): mask = segments == i result[mask] = color return result def analyze_quantization(original, quantized): """ Analyze the results of quantization """ original_colors = np.unique(original.reshape(-1, 3), axis=0) quantized_colors = np.unique(quantized.reshape(-1, 3), axis=0) stats = { "original_colors": len(original_colors), "quantized_colors": len(quantized_colors), "reduction_ratio": len(quantized_colors) / len(original_colors), } return stats