import numpy as np import cv2 from PIL import Image, ImageDraw # ---------------------------------------------------------------------- # Utility functions from the original flat pattern generator # (unchanged, except we now work with 0‑1 floats) # ---------------------------------------------------------------------- def merge_intervals(intervals): if not intervals: return [] intervals.sort(key=lambda x: x[0]) merged = [intervals[0]] for current in intervals[1:]: last = merged[-1] if current[0] <= last[1]: merged[-1] = (last[0], max(last[1], current[1])) else: merged.append(current) return merged def get_allowed_intervals(neighbor_values, delta): forbidden = [] for c in neighbor_values: low = max(0, c - delta) high = min(1, c + delta) forbidden.append((low, high)) merged = merge_intervals(forbidden) allowed = [] if merged: if merged[0][0] > 0: allowed.append((0, merged[0][0])) for i in range(1, len(merged)): allowed.append((merged[i-1][1], merged[i][0])) if merged[-1][1] < 1: allowed.append((merged[-1][1], 1)) else: allowed.append((0, 1)) return allowed def sample_from_allowed(allowed): lengths = [end - start for start, end in allowed] total_length = sum(lengths) if total_length <= 0: return None point = np.random.rand() * total_length cumulative = 0 for (start, end), length in zip(allowed, lengths): cumulative += length if point <= cumulative: return np.random.uniform(start, end) return allowed[-1][0] def sample_channel(neighbor_values, delta): allowed = get_allowed_intervals(neighbor_values, delta) if not allowed: return None return sample_from_allowed(allowed) def generate_valid_color(neighbors, r): delta = np.sqrt(r) color = np.zeros(3) for channel in range(3): neighbor_channel = [n[channel] for n in neighbors] sampled = sample_channel(neighbor_channel, delta) if sampled is None: sampled = np.random.rand() color[channel] = sampled return color # ---------------------------------------------------------------------- # Cylinder‑specific generator # ---------------------------------------------------------------------- def generate_color_pattern_cylinder( R=9.0, # cylinder radius (mm) H=20.0, # cylinder height (mm) fov_deg=150.0, # vertical FOV (degrees) pixels_per_mm=100, # texture resolution n_azimuth=36, # number of patches around the circumference n_radial=20, # number of patches vertically (visible part) r=0.2, # color randomness regulation factor output_path='cylinder_pattern.png' ): """ Generate a texture map for the inner cylinder wall. Patches are arranged so that, from a camera at the bottom looking upward with perspective projection, they appear uniform in angular and radial extent. """ # Camera half‑angle (rad) half_fov = np.radians(fov_deg / 2.0) # Visible height range (z_min = lowest point seen by camera) z_min = R / np.tan(half_fov) if half_fov < np.pi/2 else 0.0 z_max = H # Image plane radial coordinate r' = R / z r_top = R / z_max # smallest r' (top) r_bottom = R / z_min # largest r' (bottom) # Define patch grid in image space theta_step = 2 * np.pi / n_azimuth r_step = (r_bottom - r_top) / n_radial # Texture dimensions width_px = int(np.round(2 * np.pi * R * pixels_per_mm)) height_px = int(np.round(H * pixels_per_mm)) print(f"Texture size: {width_px} x {height_px} pixels") # Store patch colors in a 2D array (rows = radial index, columns = azimuth) patch_colors = np.zeros((n_radial, n_azimuth, 3), dtype=np.float32) # Generate colors row by row (top to bottom) for j in range(n_radial): # j = 0 → top, j = n_radial-1 → bottom r_start = r_top + j * r_step r_end = r_start + r_step # Corresponding z values (note: r increases → z decreases) z_start = R / r_start # larger z (higher up) z_end = R / r_end # smaller z (lower down) v_start = z_start / H v_end = z_end / H # Ensure v_start > v_end (top has larger v) if v_start < v_end: v_start, v_end = v_end, v_start for i in range(n_azimuth): theta_start = i * theta_step theta_end = theta_start + theta_step u_start = theta_start / (2 * np.pi) u_end = theta_end / (2 * np.pi) # Collect neighbor colors (above and left) neighbors = [] # Above neighbor (previous row, same column) if j > 0: neighbors.append(patch_colors[j-1, i]) # Left neighbor (same row, previous column) – wrap around left_i = (i - 1) % n_azimuth if i > 0 or j > 0: # at least one neighbor exists neighbors.append(patch_colors[j, left_i]) if len(neighbors) == 0: new_color = np.random.rand(3) else: new_color = generate_valid_color(neighbors, r) patch_colors[j, i] = new_color # Render the texture image img = Image.new('RGB', (width_px, height_px), color=(255, 255, 255)) draw = ImageDraw.Draw(img) for j in range(n_radial): r_start = r_top + j * r_step r_end = r_start + r_step z_start = R / r_start z_end = R / r_end # Determine v for top and bottom edges v_top = max(z_start, z_end) / H # larger z -> higher on cylinder v_bottom = min(z_start, z_end) / H # smaller z -> lower on cylinder for i in range(n_azimuth): u_start = i * theta_step / (2 * np.pi) u_end = (i+1) * theta_step / (2 * np.pi) # Pixel coordinates (ensure integer ordering) x0 = int(u_start * width_px) x1 = int(u_end * width_px) y0 = int(v_bottom * height_px) y1 = int(v_top * height_px) # Sanity: x0 < x1, y0 < y1 if x0 > x1: x0, x1 = x1, x0 if y0 > y1: y0, y1 = y1, y0 # Avoid degenerate rectangles (patch may span seam) if x1 <= x0: x1 = x0 + 1 if y1 <= y0: y1 = y0 + 1 color = tuple((patch_colors[j, i] * 255).astype(np.uint8)) draw.rectangle([x0, y0, x1, y1], fill=color) img.save(output_path) print(f"Saved texture to {output_path}") # Also return as numpy array (BGR for OpenCV) img_np = np.array(img)[:, :, ::-1] # RGB -> BGR return img_np # ---------------------------------------------------------------------- # Example usage # ---------------------------------------------------------------------- if __name__ == '__main__': # Generate pattern with 36 azimuth patches and 20 radial patches patch_size = 12 R = 9.0 H = 20.0 fov_deg = 150.0 pixels_per_mm = 100 n_azimuth = int(2*np.pi*R*pixels_per_mm / patch_size) n_radial = int(H*pixels_per_mm / patch_size) r = 0.05 pattern = generate_color_pattern_cylinder( R=9.0, H=20.0, fov_deg=150.0, pixels_per_mm=100, n_azimuth=n_azimuth, n_radial=n_radial, r=0.05 ) # Display with OpenCV cv2.imshow('Cylinder Color Pattern', pattern) cv2.waitKey(0) cv2.destroyAllWindows()