217 lines
7.5 KiB
Python
Executable File
217 lines
7.5 KiB
Python
Executable File
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() |