update patch generation 3d
This commit is contained in:
+38
-2
@@ -23,6 +23,37 @@ except:
|
|||||||
print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
|
print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
|
def lp_filter(series, alpha=0.5):
|
||||||
|
series = np.asarray(series)
|
||||||
|
if series.ndim == 0:
|
||||||
|
return float(series)
|
||||||
|
if series.shape[0] < 2:
|
||||||
|
return float(series[-1])
|
||||||
|
return float(series[-1] * alpha + series[-2] * (1 - alpha))
|
||||||
|
|
||||||
|
def median_filter(series, window_size=5):
|
||||||
|
series = np.asarray(series)
|
||||||
|
if series.ndim == 0:
|
||||||
|
return float(series)
|
||||||
|
if series.shape[0] == 0:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
window_n = max(1, int(window_size))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
window_n = 1
|
||||||
|
return float(np.median(series[-window_n:]))
|
||||||
|
def moving_average(series, window_size=5):
|
||||||
|
series = np.asarray(series)
|
||||||
|
if series.ndim == 0:
|
||||||
|
return float(series)
|
||||||
|
if series.shape[0] == 0:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
window_n = max(1, int(window_size))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
window_n = 1
|
||||||
|
return float(np.mean(series[-window_n:]))
|
||||||
|
|
||||||
def create_plot():
|
def create_plot():
|
||||||
p1 = win.addPlot(title="法向力")
|
p1 = win.addPlot(title="法向力")
|
||||||
p1.setLabel('bottom', '时间', units='s')
|
p1.setLabel('bottom', '时间', units='s')
|
||||||
@@ -100,7 +131,7 @@ p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot()
|
|||||||
data = np.zeros([npoints,2,3])
|
data = np.zeros([npoints,2,3])
|
||||||
start_time = None # Will be set when sensor starts
|
start_time = None # Will be set when sensor starts
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
data[:,0,i] = np.linspace(-npoints,0,npoints)
|
data[:,0,i] = np.linspace(-npoints,0,npoints)/30
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -164,6 +195,7 @@ def main():
|
|||||||
|
|
||||||
# 更新实时曲线
|
# 更新实时曲线
|
||||||
current_time = time.time() - start_time
|
current_time = time.time() - start_time
|
||||||
|
data_filtered = data.copy()
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
data[:-1, 0, i] = data[1:, 0, i] # 时间轴左移
|
data[:-1, 0, i] = data[1:, 0, i] # 时间轴左移
|
||||||
data[:-1, 1, i] = data[1:, 1, i] # 力数据左移
|
data[:-1, 1, i] = data[1:, 1, i] # 力数据左移
|
||||||
@@ -171,7 +203,11 @@ def main():
|
|||||||
data[-1, 1, 0] = fn
|
data[-1, 1, 0] = fn
|
||||||
data[-1, 1, 1] = fx
|
data[-1, 1, 1] = fx
|
||||||
data[-1, 1, 2] = fy
|
data[-1, 1, 2] = fy
|
||||||
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # 更新曲线
|
# 对最新采样点做一阶低通,滤波对象是历史曲线而不是单个标量。
|
||||||
|
data_filtered[-1, 1, 0] = moving_average(data[:, 1, 0])
|
||||||
|
data_filtered[-1, 1, 1] = moving_average(data[:, 1, 1])
|
||||||
|
data_filtered[-1, 1, 2] = moving_average(data[:, 1, 2])
|
||||||
|
update(plot_af, plot_tfx, plot_tfy, data_filtered, p1, p2, p3) # 更新曲线
|
||||||
|
|
||||||
key = cv2.waitKey(1)
|
key = cv2.waitKey(1)
|
||||||
if key & 0xFF == ord("q"):
|
if key & 0xFF == ord("q"):
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
"""
|
||||||
|
Texture Generator for Arbitrary 3D Surfaces (Blender Script)
|
||||||
|
- Mode: "patches" (black/white checker patches) or "dots" (circular markers)
|
||||||
|
- Mesh UVs are filled white so that non‑visible parts become white.
|
||||||
|
- Texture background (no‑mesh areas) stays black.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Common Utilities
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def draw_cross(pixels, width, height, cx_uv, cy_uv, length_px, thickness_px, color):
|
||||||
|
"""
|
||||||
|
Draw a cross (plus sign) in pixel space.
|
||||||
|
cx_uv, cy_uv: centre of cross in UV coordinates (0‑1), origin at bottom‑left.
|
||||||
|
length_px: total length of each arm (pixels).
|
||||||
|
thickness_px: line thickness (odd number gives symmetric result).
|
||||||
|
"""
|
||||||
|
# Convert UV centre to pixel coordinates (y from bottom)
|
||||||
|
cx = int(round(cx_uv * (width - 1)))
|
||||||
|
cy = int(round(cy_uv * (height - 1)))
|
||||||
|
|
||||||
|
half_len = length_px // 2
|
||||||
|
half_thick = thickness_px // 2
|
||||||
|
|
||||||
|
# Horizontal arm
|
||||||
|
x0 = max(0, cx - half_len)
|
||||||
|
x1 = min(width - 1, cx + half_len)
|
||||||
|
y0 = max(0, cy - half_thick)
|
||||||
|
y1 = min(height - 1, cy + half_thick)
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
# Vertical arm
|
||||||
|
x0 = max(0, cx - half_thick)
|
||||||
|
x1 = min(width - 1, cx + half_thick)
|
||||||
|
y0 = max(0, cy - half_len)
|
||||||
|
y1 = min(height - 1, cy + half_len)
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
def fill_background(pixels, width, height, color):
|
||||||
|
"""Fill the entire image with a solid RGBA color (0-1 floats)."""
|
||||||
|
r, g, b, a = color
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx] = r
|
||||||
|
pixels[idx+1] = g
|
||||||
|
pixels[idx+2] = b
|
||||||
|
pixels[idx+3] = a
|
||||||
|
|
||||||
|
def fill_polygon(image, pixels, width, height, polygon, color):
|
||||||
|
"""Fill a polygon in the pixel array (flattened, RGBA)."""
|
||||||
|
poly = [(int(round(x)), int(round(y))) for x, y in polygon]
|
||||||
|
if len(poly) < 3:
|
||||||
|
return
|
||||||
|
min_x = max(0, min(p[0] for p in poly))
|
||||||
|
max_x = min(width-1, max(p[0] for p in poly))
|
||||||
|
min_y = max(0, min(p[1] for p in poly))
|
||||||
|
max_y = min(height-1, max(p[1] for p in poly))
|
||||||
|
for y in range(min_y, max_y + 1):
|
||||||
|
intersections = []
|
||||||
|
for i in range(len(poly)):
|
||||||
|
x1, y1 = poly[i]
|
||||||
|
x2, y2 = poly[(i+1) % len(poly)]
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if (y1 <= y < y2) or (y2 <= y < y1):
|
||||||
|
t = (y - y1) / (y2 - y1)
|
||||||
|
x = x1 + t * (x2 - x1)
|
||||||
|
intersections.append(x)
|
||||||
|
intersections.sort()
|
||||||
|
for k in range(0, len(intersections), 2):
|
||||||
|
if k+1 >= len(intersections):
|
||||||
|
break
|
||||||
|
x_start = int(round(intersections[k]))
|
||||||
|
x_end = int(round(intersections[k+1]))
|
||||||
|
x_start = max(0, min(width-1, x_start))
|
||||||
|
x_end = max(0, min(width-1, x_end))
|
||||||
|
for x in range(x_start, x_end + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
|
||||||
|
def fill_polygon_value(grid, width, height, polygon, value):
|
||||||
|
"""Fill a polygon into a 2D scalar grid."""
|
||||||
|
poly = [(int(round(x)), int(round(y))) for x, y in polygon]
|
||||||
|
if len(poly) < 3:
|
||||||
|
return
|
||||||
|
min_x = max(0, min(p[0] for p in poly))
|
||||||
|
max_x = min(width - 1, max(p[0] for p in poly))
|
||||||
|
min_y = max(0, min(p[1] for p in poly))
|
||||||
|
max_y = min(height - 1, max(p[1] for p in poly))
|
||||||
|
for y in range(min_y, max_y + 1):
|
||||||
|
intersections = []
|
||||||
|
for i in range(len(poly)):
|
||||||
|
x1, y1 = poly[i]
|
||||||
|
x2, y2 = poly[(i + 1) % len(poly)]
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if (y1 <= y < y2) or (y2 <= y < y1):
|
||||||
|
t = (y - y1) / (y2 - y1)
|
||||||
|
x = x1 + t * (x2 - x1)
|
||||||
|
intersections.append(x)
|
||||||
|
intersections.sort()
|
||||||
|
for k in range(0, len(intersections), 2):
|
||||||
|
if k + 1 >= len(intersections):
|
||||||
|
break
|
||||||
|
x_start = int(round(intersections[k]))
|
||||||
|
x_end = int(round(intersections[k + 1]))
|
||||||
|
x_start = max(0, min(width - 1, x_start))
|
||||||
|
x_end = max(0, min(width - 1, x_end))
|
||||||
|
grid[y, x_start:x_end + 1] = value
|
||||||
|
|
||||||
|
def draw_ellipse(pixels, width, height, cx, cy, a, b, angle, color):
|
||||||
|
"""Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad)."""
|
||||||
|
cos_a = np.cos(angle)
|
||||||
|
sin_a = np.sin(angle)
|
||||||
|
rx = abs(a * cos_a) + abs(b * sin_a)
|
||||||
|
ry = abs(a * sin_a) + abs(b * cos_a)
|
||||||
|
x_min = int(max(0, cx - rx))
|
||||||
|
x_max = int(min(width-1, cx + rx))
|
||||||
|
y_min = int(max(0, cy - ry))
|
||||||
|
y_max = int(min(height-1, cy + ry))
|
||||||
|
for y in range(y_min, y_max + 1):
|
||||||
|
for x in range(x_min, x_max + 1):
|
||||||
|
dx = x - cx
|
||||||
|
dy = y - cy
|
||||||
|
u = dx * cos_a + dy * sin_a
|
||||||
|
v = -dx * sin_a + dy * cos_a
|
||||||
|
if (u/a)**2 + (v/b)**2 <= 1.0:
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
def get_uv_from_face(obj, face_index, location):
|
||||||
|
"""World‑space location → UV coordinate on the mesh face."""
|
||||||
|
me = obj.data
|
||||||
|
local_loc = obj.matrix_world.inverted() @ location
|
||||||
|
poly = me.polygons[face_index]
|
||||||
|
vert_indices = poly.vertices
|
||||||
|
loop_indices = poly.loop_indices
|
||||||
|
if len(vert_indices) < 3:
|
||||||
|
return None
|
||||||
|
v0 = me.vertices[vert_indices[0]].co
|
||||||
|
v1 = me.vertices[vert_indices[1]].co
|
||||||
|
v2 = me.vertices[vert_indices[2]].co
|
||||||
|
uv_layer = me.uv_layers.active.data
|
||||||
|
uv0 = uv_layer[loop_indices[0]].uv
|
||||||
|
uv1 = uv_layer[loop_indices[1]].uv
|
||||||
|
uv2 = uv_layer[loop_indices[2]].uv
|
||||||
|
def tri_area(a, b, c):
|
||||||
|
return 0.5 * ((b - a).cross(c - a)).length
|
||||||
|
total_area = tri_area(v0, v1, v2)
|
||||||
|
if total_area == 0:
|
||||||
|
return None
|
||||||
|
w0 = tri_area(local_loc, v1, v2) / total_area
|
||||||
|
w1 = tri_area(local_loc, v2, v0) / total_area
|
||||||
|
w2 = 1.0 - w0 - w1
|
||||||
|
uv = w0 * uv0 + w1 * uv1 + w2 * uv2
|
||||||
|
return Vector((uv.x, uv.y))
|
||||||
|
|
||||||
|
def raycast_from_camera(scene, depsgraph, cam, obj, u_ndc, v_ndc, frame_width, frame_height):
|
||||||
|
"""Camera raycast through normalised sensor coordinates (0‑1)."""
|
||||||
|
sx = (u_ndc - 0.5) * frame_width
|
||||||
|
sy = (v_ndc - 0.5) * frame_height
|
||||||
|
direction_cam = Vector((sx, sy, -1.0))
|
||||||
|
direction_world = cam.matrix_world.to_quaternion() @ direction_cam
|
||||||
|
direction_world.normalize()
|
||||||
|
origin = cam.matrix_world.translation
|
||||||
|
hit, location, normal, face_index, hit_obj, matrix = scene.ray_cast(
|
||||||
|
depsgraph, origin, direction_world)
|
||||||
|
if hit and hit_obj == obj:
|
||||||
|
return location, face_index
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def rasterize_mesh_uvs(obj, pixels, tex_width, tex_height, color=(1,1,1,1)):
|
||||||
|
"""
|
||||||
|
Fill all triangles of the active UV map with 'color' (default white).
|
||||||
|
This marks every part of the mesh, visible or not, as white.
|
||||||
|
"""
|
||||||
|
me = obj.data
|
||||||
|
uv_layer = me.uv_layers.active
|
||||||
|
if not uv_layer:
|
||||||
|
print("Warning: No active UV layer – mesh rasterization skipped.")
|
||||||
|
return
|
||||||
|
uv_data = uv_layer.data
|
||||||
|
for poly in me.polygons:
|
||||||
|
# Get loop indices for this polygon
|
||||||
|
loops = poly.loop_indices
|
||||||
|
# Build UV polygon
|
||||||
|
uv_poly = []
|
||||||
|
for loop_idx in loops:
|
||||||
|
uv = uv_data[loop_idx].uv
|
||||||
|
uv_poly.append((uv.x, uv.y))
|
||||||
|
# Convert UV (0‑1) to pixel coordinates
|
||||||
|
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||||
|
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize_mesh_mask(obj, tex_width, tex_height):
|
||||||
|
"""Rasterize the active UV map to a boolean mesh mask."""
|
||||||
|
mask = np.zeros((tex_height, tex_width), dtype=bool)
|
||||||
|
me = obj.data
|
||||||
|
uv_layer = me.uv_layers.active
|
||||||
|
if not uv_layer:
|
||||||
|
return mask
|
||||||
|
uv_data = uv_layer.data
|
||||||
|
for poly in me.polygons:
|
||||||
|
uv_poly = []
|
||||||
|
for loop_idx in poly.loop_indices:
|
||||||
|
uv = uv_data[loop_idx].uv
|
||||||
|
uv_poly.append((uv.x, uv.y))
|
||||||
|
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||||
|
fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def paint_mask_color(pixels, width, height, mask, color):
|
||||||
|
"""Paint all True pixels of a boolean mask with an RGBA color."""
|
||||||
|
r, g, b, a = color
|
||||||
|
ys, xs = np.nonzero(mask)
|
||||||
|
for y, x in zip(ys, xs):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = (r, g, b, a)
|
||||||
|
|
||||||
|
|
||||||
|
def complete_checker_gaps(pixels, width, height, mesh_mask, patch_state, max_gap_px=6):
|
||||||
|
"""
|
||||||
|
Fill small white slivers inside the mesh by expanding nearby checker colors.
|
||||||
|
Only grows locally (max_gap_px) so non-visible mesh areas remain white.
|
||||||
|
patch_state: -1 = unassigned, 0 = black patch, 1 = white patch
|
||||||
|
"""
|
||||||
|
for _ in range(max_gap_px):
|
||||||
|
candidate = mesh_mask & (patch_state < 0)
|
||||||
|
if not np.any(candidate):
|
||||||
|
break
|
||||||
|
|
||||||
|
black_counts = np.zeros_like(patch_state, dtype=np.uint8)
|
||||||
|
white_counts = np.zeros_like(patch_state, dtype=np.uint8)
|
||||||
|
|
||||||
|
for dy in (-1, 0, 1):
|
||||||
|
for dx in (-1, 0, 1):
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
src_y0 = max(0, -dy)
|
||||||
|
src_y1 = height - max(0, dy)
|
||||||
|
src_x0 = max(0, -dx)
|
||||||
|
src_x1 = width - max(0, dx)
|
||||||
|
dst_y0 = max(0, dy)
|
||||||
|
dst_y1 = height - max(0, -dy)
|
||||||
|
dst_x0 = max(0, dx)
|
||||||
|
dst_x1 = width - max(0, -dx)
|
||||||
|
|
||||||
|
neighbor_vals = patch_state[src_y0:src_y1, src_x0:src_x1]
|
||||||
|
black_counts[dst_y0:dst_y1, dst_x0:dst_x1] += (neighbor_vals == 0)
|
||||||
|
white_counts[dst_y0:dst_y1, dst_x0:dst_x1] += (neighbor_vals == 1)
|
||||||
|
|
||||||
|
fillable = candidate & ((black_counts + white_counts) > 0)
|
||||||
|
if not np.any(fillable):
|
||||||
|
break
|
||||||
|
|
||||||
|
patch_state[fillable] = np.where(
|
||||||
|
black_counts[fillable] > white_counts[fillable], 0, 1
|
||||||
|
).astype(np.int8)
|
||||||
|
|
||||||
|
paint_mask_color(pixels, width, height, patch_state == 0, (0.0, 0.0, 0.0, 1.0))
|
||||||
|
paint_mask_color(pixels, width, height, patch_state == 1, (1.0, 1.0, 1.0, 1.0))
|
||||||
|
|
||||||
|
def checker_patch_color(i, j):
|
||||||
|
"""Return a black/white checker color for patch index (i, j)."""
|
||||||
|
if (i + j) % 2 == 0:
|
||||||
|
return np.array([1.0, 1.0, 1.0], dtype=np.float32)
|
||||||
|
return np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Mode 1: Colored Patches
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def generate_patch_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
nx=40, ny=30, r=0.05,
|
||||||
|
texture_width=2048, texture_height=2048,
|
||||||
|
output_path="//patch_texture.png",uv_centre=None, cross_length=40, cross_thickness=3, cross_color=(1,0,0,1),
|
||||||
|
gap_fill_px=6
|
||||||
|
):
|
||||||
|
cam_data = cam.data
|
||||||
|
render = scene.render
|
||||||
|
res_x = render.resolution_x
|
||||||
|
res_y = render.resolution_y
|
||||||
|
if cam_data.sensor_fit == 'HORIZONTAL':
|
||||||
|
sensor_width = cam_data.sensor_width
|
||||||
|
sensor_height = sensor_width / (res_x / res_y)
|
||||||
|
else:
|
||||||
|
sensor_height = cam_data.sensor_height
|
||||||
|
sensor_width = sensor_height * (res_x / res_y)
|
||||||
|
focal = cam_data.lens
|
||||||
|
frame_width = sensor_width / focal
|
||||||
|
frame_height = sensor_height / focal
|
||||||
|
|
||||||
|
patch_uv_polygons = []
|
||||||
|
patch_colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
print(f"Raycasting {nx*ny} patches...")
|
||||||
|
for j in range(ny):
|
||||||
|
v_start = j / ny
|
||||||
|
v_end = (j+1) / ny
|
||||||
|
for i in range(nx):
|
||||||
|
u_start = i / nx
|
||||||
|
u_end = (i+1) / nx
|
||||||
|
corners_ndc = [(u_start, v_start), (u_end, v_start),
|
||||||
|
(u_end, v_end), (u_start, v_end)]
|
||||||
|
uv_poly = []
|
||||||
|
for (u_ndc, v_ndc) in corners_ndc:
|
||||||
|
loc, face_idx = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_ndc, v_ndc, frame_width, frame_height)
|
||||||
|
if loc is not None:
|
||||||
|
uv = get_uv_from_face(obj, face_idx, loc)
|
||||||
|
if uv is not None:
|
||||||
|
uv_poly.append(uv)
|
||||||
|
if len(uv_poly) < 3:
|
||||||
|
patch_uv_polygons.append(None)
|
||||||
|
continue
|
||||||
|
patch_uv_polygons.append(uv_poly)
|
||||||
|
|
||||||
|
patch_colors[j, i] = checker_patch_color(i, j)
|
||||||
|
|
||||||
|
# Create image – start with black background
|
||||||
|
img_name = "PatchTexture"
|
||||||
|
if img_name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[img_name])
|
||||||
|
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
||||||
|
pixels = list(image.pixels)
|
||||||
|
mesh_mask = rasterize_mesh_mask(obj, texture_width, texture_height)
|
||||||
|
patch_state = np.full((texture_height, texture_width), -1, dtype=np.int8)
|
||||||
|
|
||||||
|
# Fill entire image with black (no mesh areas)
|
||||||
|
fill_background(pixels, texture_width, texture_height, (0,0,0,1))
|
||||||
|
|
||||||
|
# Rasterize all mesh UVs in white (visible + non‑visible parts)
|
||||||
|
paint_mask_color(pixels, texture_width, texture_height, mesh_mask, (1,1,1,1))
|
||||||
|
|
||||||
|
# Now draw the visible patches on top
|
||||||
|
idx = 0
|
||||||
|
for j in range(ny):
|
||||||
|
for i in range(nx):
|
||||||
|
uv_poly = patch_uv_polygons[idx]; idx += 1
|
||||||
|
if uv_poly is None: continue
|
||||||
|
pixel_poly = [(u*(texture_width-1), v*(texture_height-1)) for u,v in uv_poly]
|
||||||
|
color = patch_colors[j,i]
|
||||||
|
rgba = (color[0], color[1], color[2], 1.0)
|
||||||
|
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||||
|
fill_polygon_value(
|
||||||
|
patch_state,
|
||||||
|
texture_width,
|
||||||
|
texture_height,
|
||||||
|
pixel_poly,
|
||||||
|
1 if np.mean(color) >= 0.5 else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fill small steep-edge gaps from neighboring checker cells.
|
||||||
|
complete_checker_gaps(
|
||||||
|
pixels, texture_width, texture_height, mesh_mask, patch_state, max_gap_px=gap_fill_px
|
||||||
|
)
|
||||||
|
|
||||||
|
# (after drawing all patches)
|
||||||
|
if uv_centre is not None:
|
||||||
|
draw_cross(pixels, texture_width, texture_height,
|
||||||
|
uv_centre[0], uv_centre[1],
|
||||||
|
cross_length, cross_thickness, cross_color)
|
||||||
|
|
||||||
|
image.pixels[:] = pixels
|
||||||
|
image.update()
|
||||||
|
abs_path = bpy.path.abspath(output_path)
|
||||||
|
image.filepath_raw = abs_path
|
||||||
|
image.file_format = 'PNG'
|
||||||
|
image.save()
|
||||||
|
print(f"Patch texture saved to {abs_path}")
|
||||||
|
return image
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Mode 2: Circular Dot Markers
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def generate_dot_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
dot_diameter_pixels=12,
|
||||||
|
spacing_pixels=40,
|
||||||
|
dot_color=(0,0,0,1), # RGBA 0-1
|
||||||
|
texture_width=2048, texture_height=2048,
|
||||||
|
output_path="//dot_texture.png",uv_centre=None, cross_length=40, cross_thickness=3, cross_color=(1,0,0,1)
|
||||||
|
):
|
||||||
|
render = scene.render
|
||||||
|
res_x = render.resolution_x
|
||||||
|
res_y = render.resolution_y
|
||||||
|
cam_data = cam.data
|
||||||
|
if cam_data.sensor_fit == 'HORIZONTAL':
|
||||||
|
sensor_width = cam_data.sensor_width
|
||||||
|
sensor_height = sensor_width / (res_x / res_y)
|
||||||
|
else:
|
||||||
|
sensor_height = cam_data.sensor_height
|
||||||
|
sensor_width = sensor_height * (res_x / res_y)
|
||||||
|
focal = cam_data.lens
|
||||||
|
frame_width = sensor_width / focal
|
||||||
|
frame_height = sensor_height / focal
|
||||||
|
|
||||||
|
margin = spacing_pixels // 2
|
||||||
|
x_centers = np.arange(margin, res_x, spacing_pixels)
|
||||||
|
y_centers = np.arange(margin, res_y, spacing_pixels)
|
||||||
|
offset_pix = max(2, dot_diameter_pixels // 4)
|
||||||
|
du_ndc = offset_pix / res_x
|
||||||
|
dv_ndc = offset_pix / res_y
|
||||||
|
|
||||||
|
dots = [] # (cx_uv, cy_uv, a_uv, b_uv, angle_uv)
|
||||||
|
print(f"Raycasting {len(x_centers)*len(y_centers)} dot centers...")
|
||||||
|
for y_pix in y_centers:
|
||||||
|
v_ndc = y_pix / res_y
|
||||||
|
for x_pix in x_centers:
|
||||||
|
u_ndc = x_pix / res_x
|
||||||
|
center_loc, face_idx = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_ndc, v_ndc, frame_width, frame_height)
|
||||||
|
if center_loc is None: continue
|
||||||
|
center_uv = get_uv_from_face(obj, face_idx, center_loc)
|
||||||
|
if center_uv is None: continue
|
||||||
|
|
||||||
|
offsets = [
|
||||||
|
( u_ndc + du_ndc, v_ndc),
|
||||||
|
( u_ndc - du_ndc, v_ndc),
|
||||||
|
( u_ndc, v_ndc + dv_ndc),
|
||||||
|
( u_ndc, v_ndc - dv_ndc)
|
||||||
|
]
|
||||||
|
uv_offsets = []
|
||||||
|
valid = True
|
||||||
|
for (u_off, v_off) in offsets:
|
||||||
|
loc_off, f_idx_off = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_off, v_off, frame_width, frame_height)
|
||||||
|
if loc_off is None:
|
||||||
|
valid = False; break
|
||||||
|
uv_off = get_uv_from_face(obj, f_idx_off, loc_off)
|
||||||
|
if uv_off is None:
|
||||||
|
valid = False; break
|
||||||
|
uv_offsets.append(uv_off)
|
||||||
|
if not valid: continue
|
||||||
|
|
||||||
|
scale_u = 1.0 / res_x
|
||||||
|
scale_v = 1.0 / res_y
|
||||||
|
du_dx = (uv_offsets[0] - uv_offsets[1]) / (2 * offset_pix * scale_u)
|
||||||
|
dv_dx = (uv_offsets[2] - uv_offsets[3]) / (2 * offset_pix * scale_v)
|
||||||
|
r_pix = dot_diameter_pixels / 2.0
|
||||||
|
a_uv = r_pix * du_dx.length * scale_u
|
||||||
|
b_uv = r_pix * dv_dx.length * scale_v
|
||||||
|
angle = np.arctan2(du_dx.y, du_dx.x) if du_dx.length > 0 else 0.0
|
||||||
|
dots.append((center_uv.x, center_uv.y, a_uv, b_uv, angle))
|
||||||
|
|
||||||
|
print(f"Found {len(dots)} valid dots.")
|
||||||
|
|
||||||
|
if uv_centre is not None:
|
||||||
|
draw_cross(pixels, texture_width, texture_height,
|
||||||
|
uv_centre[0], uv_centre[1],
|
||||||
|
cross_length, cross_thickness, cross_color)
|
||||||
|
|
||||||
|
# Create image
|
||||||
|
img_name = "DotTexture"
|
||||||
|
if img_name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[img_name])
|
||||||
|
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
||||||
|
pixels = list(image.pixels)
|
||||||
|
|
||||||
|
# Black background (no mesh)
|
||||||
|
fill_background(pixels, texture_width, texture_height, (0,0,0,1))
|
||||||
|
# White for all mesh areas
|
||||||
|
rasterize_mesh_uvs(obj, pixels, texture_width, texture_height, color=(1,1,1,1))
|
||||||
|
# Draw dots
|
||||||
|
for (cx_uv, cy_uv, a_uv, b_uv, angle_uv) in dots:
|
||||||
|
cx_px = cx_uv * (texture_width - 1)
|
||||||
|
cy_px = cy_uv * (texture_height - 1)
|
||||||
|
a_px = a_uv * texture_width
|
||||||
|
b_px = b_uv * texture_height
|
||||||
|
draw_ellipse(pixels, texture_width, texture_height,
|
||||||
|
cx_px, cy_px, a_px, b_px, angle_uv, dot_color)
|
||||||
|
|
||||||
|
image.pixels[:] = pixels
|
||||||
|
image.update()
|
||||||
|
abs_path = bpy.path.abspath(output_path)
|
||||||
|
image.filepath_raw = abs_path
|
||||||
|
image.file_format = 'PNG'
|
||||||
|
image.save()
|
||||||
|
print(f"Dot texture saved to {abs_path}")
|
||||||
|
return image
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
# ==================== CONFIGURATION ====================
|
||||||
|
MODE = "patches"
|
||||||
|
OBJ_NAME = "FingerMesh"
|
||||||
|
CAM_NAME = "Camera"
|
||||||
|
TEXTURE_WIDTH = 4096
|
||||||
|
TEXTURE_HEIGHT = 4096
|
||||||
|
OUTPUT_PATH = "//" + ("patch_texture.png" if MODE=="patches" else "dot_texture.png")
|
||||||
|
|
||||||
|
# Patch mode
|
||||||
|
PATCH_NX = 40; PATCH_NY = 40; COLOR_R = 0.05
|
||||||
|
# Dot mode
|
||||||
|
DOT_DIAMETER_PIX = 12; DOT_SPACING_PIX = 40; DOT_COLOR = (0,0,0,1)
|
||||||
|
# Cross
|
||||||
|
CROSS_LENGTH_PX = 40
|
||||||
|
CROSS_THICKNESS_PX = 3
|
||||||
|
CROSS_COLOR = (1, 0, 0, 1) # red cross
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
scene = bpy.context.scene
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
|
if OBJ_NAME:
|
||||||
|
obj = bpy.data.objects.get(OBJ_NAME)
|
||||||
|
else:
|
||||||
|
obj = bpy.context.active_object
|
||||||
|
if obj is None or obj.type != 'MESH':
|
||||||
|
raise ValueError("No valid mesh object found.")
|
||||||
|
|
||||||
|
cam = bpy.data.objects.get(CAM_NAME) if CAM_NAME else scene.camera
|
||||||
|
if cam is None or cam.type != 'CAMERA':
|
||||||
|
raise ValueError("No camera found.")
|
||||||
|
|
||||||
|
if not obj.data.uv_layers.active:
|
||||||
|
obj.data.uv_layers.new(name="UVMap")
|
||||||
|
|
||||||
|
# Compute UV centre
|
||||||
|
uv_layer = obj.data.uv_layers.active
|
||||||
|
uv_coords = np.array([uv_layer.data[i].uv.to_tuple() for i in range(len(uv_layer.data))])
|
||||||
|
uv_min = uv_coords.min(axis=0)
|
||||||
|
uv_max = uv_coords.max(axis=0)
|
||||||
|
uv_centre = (uv_min + uv_max) / 2.0
|
||||||
|
print(f"UV centre: ({uv_centre[0]:.4f}, {uv_centre[1]:.4f})")
|
||||||
|
|
||||||
|
|
||||||
|
# Compute UV bounding box centre
|
||||||
|
me = obj.data
|
||||||
|
uv_layer = me.uv_layers.active
|
||||||
|
uv_coords = np.array([uv_layer.data[i].uv.to_tuple() for i in range(len(uv_layer.data))])
|
||||||
|
uv_min = uv_coords.min(axis=0)
|
||||||
|
uv_max = uv_coords.max(axis=0)
|
||||||
|
uv_centre = (uv_min + uv_max) / 2.0
|
||||||
|
print(f"UV centre: {uv_centre[0]:.4f}, {uv_centre[1]:.4f}")
|
||||||
|
|
||||||
|
if MODE == "patches":
|
||||||
|
generate_patch_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
nx=PATCH_NX, ny=PATCH_NY, r=COLOR_R,
|
||||||
|
texture_width=TEXTURE_WIDTH, texture_height=TEXTURE_HEIGHT,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
uv_centre=uv_centre,
|
||||||
|
cross_length=CROSS_LENGTH_PX,
|
||||||
|
cross_thickness=CROSS_THICKNESS_PX,
|
||||||
|
cross_color=CROSS_COLOR
|
||||||
|
)
|
||||||
|
elif MODE == "dots":
|
||||||
|
generate_dot_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
dot_diameter_pixels=DOT_DIAMETER_PIX,
|
||||||
|
spacing_pixels=DOT_SPACING_PIX,
|
||||||
|
dot_color=DOT_COLOR,
|
||||||
|
texture_width=TEXTURE_WIDTH, texture_height=TEXTURE_HEIGHT,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
uv_centre=uv_centre,
|
||||||
|
cross_length=CROSS_LENGTH_PX,
|
||||||
|
cross_thickness=CROSS_THICKNESS_PX,
|
||||||
|
cross_color=CROSS_COLOR
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Completed in {time.time()-start_time:.2f} seconds.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
@@ -0,0 +1,528 @@
|
|||||||
|
"""
|
||||||
|
Texture Generator for Arbitrary 3D Surfaces (Blender Script)
|
||||||
|
- Mode: "patches" (colored patches) or "dots" (circular markers)
|
||||||
|
- Mesh UVs are filled white so that non‑visible parts become white.
|
||||||
|
- Texture background (no‑mesh areas) stays black.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Common Utilities
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def draw_cross(pixels, width, height, cx_uv, cy_uv, length_px, thickness_px, color):
|
||||||
|
"""
|
||||||
|
Draw a cross (plus sign) in pixel space.
|
||||||
|
cx_uv, cy_uv: centre of cross in UV coordinates (0‑1), origin at bottom‑left.
|
||||||
|
length_px: total length of each arm (pixels).
|
||||||
|
thickness_px: line thickness (odd number gives symmetric result).
|
||||||
|
"""
|
||||||
|
# Convert UV centre to pixel coordinates (y from bottom)
|
||||||
|
cx = int(round(cx_uv * (width - 1)))
|
||||||
|
cy = int(round(cy_uv * (height - 1)))
|
||||||
|
|
||||||
|
half_len = length_px // 2
|
||||||
|
half_thick = thickness_px // 2
|
||||||
|
|
||||||
|
# Horizontal arm
|
||||||
|
x0 = max(0, cx - half_len)
|
||||||
|
x1 = min(width - 1, cx + half_len)
|
||||||
|
y0 = max(0, cy - half_thick)
|
||||||
|
y1 = min(height - 1, cy + half_thick)
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
# Vertical arm
|
||||||
|
x0 = max(0, cx - half_thick)
|
||||||
|
x1 = min(width - 1, cx + half_thick)
|
||||||
|
y0 = max(0, cy - half_len)
|
||||||
|
y1 = min(height - 1, cy + half_len)
|
||||||
|
for y in range(y0, y1 + 1):
|
||||||
|
for x in range(x0, x1 + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
def fill_background(pixels, width, height, color):
|
||||||
|
"""Fill the entire image with a solid RGBA color (0-1 floats)."""
|
||||||
|
r, g, b, a = color
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx] = r
|
||||||
|
pixels[idx+1] = g
|
||||||
|
pixels[idx+2] = b
|
||||||
|
pixels[idx+3] = a
|
||||||
|
|
||||||
|
def fill_polygon(image, pixels, width, height, polygon, color):
|
||||||
|
"""Fill a polygon in the pixel array (flattened, RGBA)."""
|
||||||
|
poly = [(int(round(x)), int(round(y))) for x, y in polygon]
|
||||||
|
if len(poly) < 3:
|
||||||
|
return
|
||||||
|
min_x = max(0, min(p[0] for p in poly))
|
||||||
|
max_x = min(width-1, max(p[0] for p in poly))
|
||||||
|
min_y = max(0, min(p[1] for p in poly))
|
||||||
|
max_y = min(height-1, max(p[1] for p in poly))
|
||||||
|
for y in range(min_y, max_y + 1):
|
||||||
|
intersections = []
|
||||||
|
for i in range(len(poly)):
|
||||||
|
x1, y1 = poly[i]
|
||||||
|
x2, y2 = poly[(i+1) % len(poly)]
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if (y1 <= y < y2) or (y2 <= y < y1):
|
||||||
|
t = (y - y1) / (y2 - y1)
|
||||||
|
x = x1 + t * (x2 - x1)
|
||||||
|
intersections.append(x)
|
||||||
|
intersections.sort()
|
||||||
|
for k in range(0, len(intersections), 2):
|
||||||
|
if k+1 >= len(intersections):
|
||||||
|
break
|
||||||
|
x_start = int(round(intersections[k]))
|
||||||
|
x_end = int(round(intersections[k+1]))
|
||||||
|
x_start = max(0, min(width-1, x_start))
|
||||||
|
x_end = max(0, min(width-1, x_end))
|
||||||
|
for x in range(x_start, x_end + 1):
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
def draw_ellipse(pixels, width, height, cx, cy, a, b, angle, color):
|
||||||
|
"""Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad)."""
|
||||||
|
cos_a = np.cos(angle)
|
||||||
|
sin_a = np.sin(angle)
|
||||||
|
rx = abs(a * cos_a) + abs(b * sin_a)
|
||||||
|
ry = abs(a * sin_a) + abs(b * cos_a)
|
||||||
|
x_min = int(max(0, cx - rx))
|
||||||
|
x_max = int(min(width-1, cx + rx))
|
||||||
|
y_min = int(max(0, cy - ry))
|
||||||
|
y_max = int(min(height-1, cy + ry))
|
||||||
|
for y in range(y_min, y_max + 1):
|
||||||
|
for x in range(x_min, x_max + 1):
|
||||||
|
dx = x - cx
|
||||||
|
dy = y - cy
|
||||||
|
u = dx * cos_a + dy * sin_a
|
||||||
|
v = -dx * sin_a + dy * cos_a
|
||||||
|
if (u/a)**2 + (v/b)**2 <= 1.0:
|
||||||
|
idx = (y * width + x) * 4
|
||||||
|
pixels[idx:idx+4] = color
|
||||||
|
|
||||||
|
def get_uv_from_face(obj, face_index, location):
|
||||||
|
"""World‑space location → UV coordinate on the mesh face."""
|
||||||
|
me = obj.data
|
||||||
|
local_loc = obj.matrix_world.inverted() @ location
|
||||||
|
poly = me.polygons[face_index]
|
||||||
|
vert_indices = poly.vertices
|
||||||
|
loop_indices = poly.loop_indices
|
||||||
|
if len(vert_indices) < 3:
|
||||||
|
return None
|
||||||
|
v0 = me.vertices[vert_indices[0]].co
|
||||||
|
v1 = me.vertices[vert_indices[1]].co
|
||||||
|
v2 = me.vertices[vert_indices[2]].co
|
||||||
|
uv_layer = me.uv_layers.active.data
|
||||||
|
uv0 = uv_layer[loop_indices[0]].uv
|
||||||
|
uv1 = uv_layer[loop_indices[1]].uv
|
||||||
|
uv2 = uv_layer[loop_indices[2]].uv
|
||||||
|
def tri_area(a, b, c):
|
||||||
|
return 0.5 * ((b - a).cross(c - a)).length
|
||||||
|
total_area = tri_area(v0, v1, v2)
|
||||||
|
if total_area == 0:
|
||||||
|
return None
|
||||||
|
w0 = tri_area(local_loc, v1, v2) / total_area
|
||||||
|
w1 = tri_area(local_loc, v2, v0) / total_area
|
||||||
|
w2 = 1.0 - w0 - w1
|
||||||
|
uv = w0 * uv0 + w1 * uv1 + w2 * uv2
|
||||||
|
return Vector((uv.x, uv.y))
|
||||||
|
|
||||||
|
def raycast_from_camera(scene, depsgraph, cam, obj, u_ndc, v_ndc, frame_width, frame_height):
|
||||||
|
"""Camera raycast through normalised sensor coordinates (0‑1)."""
|
||||||
|
sx = (u_ndc - 0.5) * frame_width
|
||||||
|
sy = (v_ndc - 0.5) * frame_height
|
||||||
|
direction_cam = Vector((sx, sy, -1.0))
|
||||||
|
direction_world = cam.matrix_world.to_quaternion() @ direction_cam
|
||||||
|
direction_world.normalize()
|
||||||
|
origin = cam.matrix_world.translation
|
||||||
|
hit, location, normal, face_index, hit_obj, matrix = scene.ray_cast(
|
||||||
|
depsgraph, origin, direction_world)
|
||||||
|
if hit and hit_obj == obj:
|
||||||
|
return location, face_index
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def rasterize_mesh_uvs(obj, pixels, tex_width, tex_height, color=(1,1,1,1)):
|
||||||
|
"""
|
||||||
|
Fill all triangles of the active UV map with 'color' (default white).
|
||||||
|
This marks every part of the mesh, visible or not, as white.
|
||||||
|
"""
|
||||||
|
me = obj.data
|
||||||
|
uv_layer = me.uv_layers.active
|
||||||
|
if not uv_layer:
|
||||||
|
print("Warning: No active UV layer – mesh rasterization skipped.")
|
||||||
|
return
|
||||||
|
uv_data = uv_layer.data
|
||||||
|
for poly in me.polygons:
|
||||||
|
# Get loop indices for this polygon
|
||||||
|
loops = poly.loop_indices
|
||||||
|
# Build UV polygon
|
||||||
|
uv_poly = []
|
||||||
|
for loop_idx in loops:
|
||||||
|
uv = uv_data[loop_idx].uv
|
||||||
|
uv_poly.append((uv.x, uv.y))
|
||||||
|
# Convert UV (0‑1) to pixel coordinates
|
||||||
|
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||||
|
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Color generation (for patch mode)
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
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
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Mode 1: Colored Patches
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def generate_patch_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
nx=40, ny=30, r=0.05,
|
||||||
|
texture_width=2048, texture_height=2048,
|
||||||
|
output_path="//patch_texture.png",uv_centre=None, cross_length=40, cross_thickness=3, cross_color=(1,0,0,1)
|
||||||
|
):
|
||||||
|
cam_data = cam.data
|
||||||
|
render = scene.render
|
||||||
|
res_x = render.resolution_x
|
||||||
|
res_y = render.resolution_y
|
||||||
|
if cam_data.sensor_fit == 'HORIZONTAL':
|
||||||
|
sensor_width = cam_data.sensor_width
|
||||||
|
sensor_height = sensor_width / (res_x / res_y)
|
||||||
|
else:
|
||||||
|
sensor_height = cam_data.sensor_height
|
||||||
|
sensor_width = sensor_height * (res_x / res_y)
|
||||||
|
focal = cam_data.lens
|
||||||
|
frame_width = sensor_width / focal
|
||||||
|
frame_height = sensor_height / focal
|
||||||
|
|
||||||
|
patch_uv_polygons = []
|
||||||
|
patch_colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
print(f"Raycasting {nx*ny} patches...")
|
||||||
|
for j in range(ny):
|
||||||
|
v_start = j / ny
|
||||||
|
v_end = (j+1) / ny
|
||||||
|
for i in range(nx):
|
||||||
|
u_start = i / nx
|
||||||
|
u_end = (i+1) / nx
|
||||||
|
corners_ndc = [(u_start, v_start), (u_end, v_start),
|
||||||
|
(u_end, v_end), (u_start, v_end)]
|
||||||
|
uv_poly = []
|
||||||
|
for (u_ndc, v_ndc) in corners_ndc:
|
||||||
|
loc, face_idx = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_ndc, v_ndc, frame_width, frame_height)
|
||||||
|
if loc is not None:
|
||||||
|
uv = get_uv_from_face(obj, face_idx, loc)
|
||||||
|
if uv is not None:
|
||||||
|
uv_poly.append(uv)
|
||||||
|
if len(uv_poly) < 3:
|
||||||
|
patch_uv_polygons.append(None)
|
||||||
|
continue
|
||||||
|
patch_uv_polygons.append(uv_poly)
|
||||||
|
|
||||||
|
neighbors = []
|
||||||
|
if j > 0: neighbors.append(patch_colors[j-1, i])
|
||||||
|
if i > 0: neighbors.append(patch_colors[j, i-1])
|
||||||
|
color = np.random.rand(3) if len(neighbors)==0 else generate_valid_color(neighbors, r)
|
||||||
|
patch_colors[j, i] = color
|
||||||
|
|
||||||
|
# Create image – start with black background
|
||||||
|
img_name = "PatchTexture"
|
||||||
|
if img_name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[img_name])
|
||||||
|
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
||||||
|
pixels = list(image.pixels)
|
||||||
|
|
||||||
|
# Fill entire image with black (no mesh areas)
|
||||||
|
fill_background(pixels, texture_width, texture_height, (0,0,0,1))
|
||||||
|
|
||||||
|
# Rasterize all mesh UVs in white (visible + non‑visible parts)
|
||||||
|
rasterize_mesh_uvs(obj, pixels, texture_width, texture_height, color=(1,1,1,1))
|
||||||
|
|
||||||
|
# Now draw the visible patches on top
|
||||||
|
idx = 0
|
||||||
|
for j in range(ny):
|
||||||
|
for i in range(nx):
|
||||||
|
uv_poly = patch_uv_polygons[idx]; idx += 1
|
||||||
|
if uv_poly is None: continue
|
||||||
|
pixel_poly = [(u*(texture_width-1), v*(texture_height-1)) for u,v in uv_poly]
|
||||||
|
color = patch_colors[j,i]
|
||||||
|
rgba = (color[0], color[1], color[2], 1.0)
|
||||||
|
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||||
|
|
||||||
|
# (after drawing all patches)
|
||||||
|
if uv_centre is not None:
|
||||||
|
draw_cross(pixels, texture_width, texture_height,
|
||||||
|
uv_centre[0], uv_centre[1],
|
||||||
|
cross_length, cross_thickness, cross_color)
|
||||||
|
|
||||||
|
image.pixels[:] = pixels
|
||||||
|
image.update()
|
||||||
|
abs_path = bpy.path.abspath(output_path)
|
||||||
|
image.filepath_raw = abs_path
|
||||||
|
image.file_format = 'PNG'
|
||||||
|
image.save()
|
||||||
|
print(f"Patch texture saved to {abs_path}")
|
||||||
|
return image
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Mode 2: Circular Dot Markers
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def generate_dot_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
dot_diameter_pixels=12,
|
||||||
|
spacing_pixels=40,
|
||||||
|
dot_color=(0,0,0,1), # RGBA 0-1
|
||||||
|
texture_width=2048, texture_height=2048,
|
||||||
|
output_path="//dot_texture.png",uv_centre=None, cross_length=40, cross_thickness=3, cross_color=(1,0,0,1)
|
||||||
|
):
|
||||||
|
render = scene.render
|
||||||
|
res_x = render.resolution_x
|
||||||
|
res_y = render.resolution_y
|
||||||
|
cam_data = cam.data
|
||||||
|
if cam_data.sensor_fit == 'HORIZONTAL':
|
||||||
|
sensor_width = cam_data.sensor_width
|
||||||
|
sensor_height = sensor_width / (res_x / res_y)
|
||||||
|
else:
|
||||||
|
sensor_height = cam_data.sensor_height
|
||||||
|
sensor_width = sensor_height * (res_x / res_y)
|
||||||
|
focal = cam_data.lens
|
||||||
|
frame_width = sensor_width / focal
|
||||||
|
frame_height = sensor_height / focal
|
||||||
|
|
||||||
|
margin = spacing_pixels // 2
|
||||||
|
x_centers = np.arange(margin, res_x, spacing_pixels)
|
||||||
|
y_centers = np.arange(margin, res_y, spacing_pixels)
|
||||||
|
offset_pix = max(2, dot_diameter_pixels // 4)
|
||||||
|
du_ndc = offset_pix / res_x
|
||||||
|
dv_ndc = offset_pix / res_y
|
||||||
|
|
||||||
|
dots = [] # (cx_uv, cy_uv, a_uv, b_uv, angle_uv)
|
||||||
|
print(f"Raycasting {len(x_centers)*len(y_centers)} dot centers...")
|
||||||
|
for y_pix in y_centers:
|
||||||
|
v_ndc = y_pix / res_y
|
||||||
|
for x_pix in x_centers:
|
||||||
|
u_ndc = x_pix / res_x
|
||||||
|
center_loc, face_idx = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_ndc, v_ndc, frame_width, frame_height)
|
||||||
|
if center_loc is None: continue
|
||||||
|
center_uv = get_uv_from_face(obj, face_idx, center_loc)
|
||||||
|
if center_uv is None: continue
|
||||||
|
|
||||||
|
offsets = [
|
||||||
|
( u_ndc + du_ndc, v_ndc),
|
||||||
|
( u_ndc - du_ndc, v_ndc),
|
||||||
|
( u_ndc, v_ndc + dv_ndc),
|
||||||
|
( u_ndc, v_ndc - dv_ndc)
|
||||||
|
]
|
||||||
|
uv_offsets = []
|
||||||
|
valid = True
|
||||||
|
for (u_off, v_off) in offsets:
|
||||||
|
loc_off, f_idx_off = raycast_from_camera(scene, depsgraph, cam, obj,
|
||||||
|
u_off, v_off, frame_width, frame_height)
|
||||||
|
if loc_off is None:
|
||||||
|
valid = False; break
|
||||||
|
uv_off = get_uv_from_face(obj, f_idx_off, loc_off)
|
||||||
|
if uv_off is None:
|
||||||
|
valid = False; break
|
||||||
|
uv_offsets.append(uv_off)
|
||||||
|
if not valid: continue
|
||||||
|
|
||||||
|
scale_u = 1.0 / res_x
|
||||||
|
scale_v = 1.0 / res_y
|
||||||
|
du_dx = (uv_offsets[0] - uv_offsets[1]) / (2 * offset_pix * scale_u)
|
||||||
|
dv_dx = (uv_offsets[2] - uv_offsets[3]) / (2 * offset_pix * scale_v)
|
||||||
|
r_pix = dot_diameter_pixels / 2.0
|
||||||
|
a_uv = r_pix * du_dx.length * scale_u
|
||||||
|
b_uv = r_pix * dv_dx.length * scale_v
|
||||||
|
angle = np.arctan2(du_dx.y, du_dx.x) if du_dx.length > 0 else 0.0
|
||||||
|
dots.append((center_uv.x, center_uv.y, a_uv, b_uv, angle))
|
||||||
|
|
||||||
|
print(f"Found {len(dots)} valid dots.")
|
||||||
|
|
||||||
|
if uv_centre is not None:
|
||||||
|
draw_cross(pixels, texture_width, texture_height,
|
||||||
|
uv_centre[0], uv_centre[1],
|
||||||
|
cross_length, cross_thickness, cross_color)
|
||||||
|
|
||||||
|
# Create image
|
||||||
|
img_name = "DotTexture"
|
||||||
|
if img_name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[img_name])
|
||||||
|
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
||||||
|
pixels = list(image.pixels)
|
||||||
|
|
||||||
|
# Black background (no mesh)
|
||||||
|
fill_background(pixels, texture_width, texture_height, (0,0,0,1))
|
||||||
|
# White for all mesh areas
|
||||||
|
rasterize_mesh_uvs(obj, pixels, texture_width, texture_height, color=(1,1,1,1))
|
||||||
|
# Draw dots
|
||||||
|
for (cx_uv, cy_uv, a_uv, b_uv, angle_uv) in dots:
|
||||||
|
cx_px = cx_uv * (texture_width - 1)
|
||||||
|
cy_px = cy_uv * (texture_height - 1)
|
||||||
|
a_px = a_uv * texture_width
|
||||||
|
b_px = b_uv * texture_height
|
||||||
|
draw_ellipse(pixels, texture_width, texture_height,
|
||||||
|
cx_px, cy_px, a_px, b_px, angle_uv, dot_color)
|
||||||
|
|
||||||
|
image.pixels[:] = pixels
|
||||||
|
image.update()
|
||||||
|
abs_path = bpy.path.abspath(output_path)
|
||||||
|
image.filepath_raw = abs_path
|
||||||
|
image.file_format = 'PNG'
|
||||||
|
image.save()
|
||||||
|
print(f"Dot texture saved to {abs_path}")
|
||||||
|
return image
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
# ==================== CONFIGURATION ====================
|
||||||
|
MODE = "patches"
|
||||||
|
OBJ_NAME = "FingerMesh"
|
||||||
|
CAM_NAME = "Camera"
|
||||||
|
TEXTURE_WIDTH = 4096
|
||||||
|
TEXTURE_HEIGHT = 4096
|
||||||
|
OUTPUT_PATH = "//" + ("patch_texture.png" if MODE=="patches" else "dot_texture.png")
|
||||||
|
|
||||||
|
# Patch mode
|
||||||
|
PATCH_NX = 60; PATCH_NY = 50; COLOR_R = 0.05
|
||||||
|
# Dot mode
|
||||||
|
DOT_DIAMETER_PIX = 12; DOT_SPACING_PIX = 40; DOT_COLOR = (0,0,0,1)
|
||||||
|
# Cross
|
||||||
|
CROSS_LENGTH_PX = 40
|
||||||
|
CROSS_THICKNESS_PX = 3
|
||||||
|
CROSS_COLOR = (1, 0, 0, 1) # red cross
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
scene = bpy.context.scene
|
||||||
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
|
if OBJ_NAME:
|
||||||
|
obj = bpy.data.objects.get(OBJ_NAME)
|
||||||
|
else:
|
||||||
|
obj = bpy.context.active_object
|
||||||
|
if obj is None or obj.type != 'MESH':
|
||||||
|
raise ValueError("No valid mesh object found.")
|
||||||
|
|
||||||
|
cam = bpy.data.objects.get(CAM_NAME) if CAM_NAME else scene.camera
|
||||||
|
if cam is None or cam.type != 'CAMERA':
|
||||||
|
raise ValueError("No camera found.")
|
||||||
|
|
||||||
|
if not obj.data.uv_layers.active:
|
||||||
|
obj.data.uv_layers.new(name="UVMap")
|
||||||
|
|
||||||
|
# Compute UV centre
|
||||||
|
uv_layer = obj.data.uv_layers.active
|
||||||
|
uv_coords = np.array([uv_layer.data[i].uv.to_tuple() for i in range(len(uv_layer.data))])
|
||||||
|
uv_min = uv_coords.min(axis=0)
|
||||||
|
uv_max = uv_coords.max(axis=0)
|
||||||
|
uv_centre = (uv_min + uv_max) / 2.0
|
||||||
|
print(f"UV centre: ({uv_centre[0]:.4f}, {uv_centre[1]:.4f})")
|
||||||
|
|
||||||
|
|
||||||
|
# Compute UV bounding box centre
|
||||||
|
me = obj.data
|
||||||
|
uv_layer = me.uv_layers.active
|
||||||
|
uv_coords = np.array([uv_layer.data[i].uv.to_tuple() for i in range(len(uv_layer.data))])
|
||||||
|
uv_min = uv_coords.min(axis=0)
|
||||||
|
uv_max = uv_coords.max(axis=0)
|
||||||
|
uv_centre = (uv_min + uv_max) / 2.0
|
||||||
|
print(f"UV centre: {uv_centre[0]:.4f}, {uv_centre[1]:.4f}")
|
||||||
|
|
||||||
|
if MODE == "patches":
|
||||||
|
generate_patch_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
nx=PATCH_NX, ny=PATCH_NY, r=COLOR_R,
|
||||||
|
texture_width=TEXTURE_WIDTH, texture_height=TEXTURE_HEIGHT,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
uv_centre=uv_centre,
|
||||||
|
cross_length=CROSS_LENGTH_PX,
|
||||||
|
cross_thickness=CROSS_THICKNESS_PX,
|
||||||
|
cross_color=CROSS_COLOR
|
||||||
|
)
|
||||||
|
elif MODE == "dots":
|
||||||
|
generate_dot_texture(
|
||||||
|
obj, cam, scene, depsgraph,
|
||||||
|
dot_diameter_pixels=DOT_DIAMETER_PIX,
|
||||||
|
spacing_pixels=DOT_SPACING_PIX,
|
||||||
|
dot_color=DOT_COLOR,
|
||||||
|
texture_width=TEXTURE_WIDTH, texture_height=TEXTURE_HEIGHT,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
uv_centre=uv_centre,
|
||||||
|
cross_length=CROSS_LENGTH_PX,
|
||||||
|
cross_thickness=CROSS_THICKNESS_PX,
|
||||||
|
cross_color=CROSS_COLOR
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Completed in {time.time()-start_time:.2f} seconds.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
import numpy as np
|
|
||||||
from mathutils import Vector
|
|
||||||
import os
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Scanline polygon fill for Blender Image pixels
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def fill_polygon(image, pixels, width, height, polygon, color):
|
|
||||||
poly = [(int(round(x)), int(round(y))) for x, y in polygon]
|
|
||||||
if len(poly) < 3:
|
|
||||||
return
|
|
||||||
|
|
||||||
min_x = max(0, min(p[0] for p in poly))
|
|
||||||
max_x = min(width-1, max(p[0] for p in poly))
|
|
||||||
min_y = max(0, min(p[1] for p in poly))
|
|
||||||
max_y = min(height-1, max(p[1] for p in poly))
|
|
||||||
|
|
||||||
for y in range(min_y, max_y + 1):
|
|
||||||
intersections = []
|
|
||||||
for i in range(len(poly)):
|
|
||||||
x1, y1 = poly[i]
|
|
||||||
x2, y2 = poly[(i+1) % len(poly)]
|
|
||||||
if y1 == y2:
|
|
||||||
continue
|
|
||||||
if (y1 <= y < y2) or (y2 <= y < y1):
|
|
||||||
t = (y - y1) / (y2 - y1)
|
|
||||||
x = x1 + t * (x2 - x1)
|
|
||||||
intersections.append(x)
|
|
||||||
intersections.sort()
|
|
||||||
for k in range(0, len(intersections), 2):
|
|
||||||
if k+1 >= len(intersections):
|
|
||||||
break
|
|
||||||
x_start = int(round(intersections[k]))
|
|
||||||
x_end = int(round(intersections[k+1]))
|
|
||||||
x_start = max(0, min(width-1, x_start))
|
|
||||||
x_end = max(0, min(width-1, x_end))
|
|
||||||
for x in range(x_start, x_end + 1):
|
|
||||||
idx = (y * width + x) * 4
|
|
||||||
pixels[idx:idx+4] = color
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Get interpolated UV from face and hit location (fixed version)
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def get_uv_from_face(obj, face_index, location):
|
|
||||||
me = obj.data
|
|
||||||
local_loc = obj.matrix_world.inverted() @ location
|
|
||||||
|
|
||||||
poly = me.polygons[face_index]
|
|
||||||
vert_indices = poly.vertices
|
|
||||||
loop_indices = poly.loop_indices
|
|
||||||
|
|
||||||
if len(vert_indices) < 3:
|
|
||||||
return None
|
|
||||||
|
|
||||||
v0 = me.vertices[vert_indices[0]].co
|
|
||||||
v1 = me.vertices[vert_indices[1]].co
|
|
||||||
v2 = me.vertices[vert_indices[2]].co
|
|
||||||
|
|
||||||
uv_layer = me.uv_layers.active.data
|
|
||||||
uv0 = uv_layer[loop_indices[0]].uv
|
|
||||||
uv1 = uv_layer[loop_indices[1]].uv
|
|
||||||
uv2 = uv_layer[loop_indices[2]].uv
|
|
||||||
|
|
||||||
def tri_area(a, b, c):
|
|
||||||
return 0.5 * ((b - a).cross(c - a)).length
|
|
||||||
|
|
||||||
total_area = tri_area(v0, v1, v2)
|
|
||||||
if total_area == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
w0 = tri_area(local_loc, v1, v2) / total_area
|
|
||||||
w1 = tri_area(local_loc, v2, v0) / total_area
|
|
||||||
w2 = 1.0 - w0 - w1
|
|
||||||
|
|
||||||
uv = w0 * uv0 + w1 * uv1 + w2 * uv2
|
|
||||||
return Vector((uv.x, uv.y))
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Color generation functions (unchanged)
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
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
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Main function
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def generate_fingerprint_texture(
|
|
||||||
obj_name=None,
|
|
||||||
camera_name="Camera",
|
|
||||||
nx=40,
|
|
||||||
ny=30,
|
|
||||||
r=0.05,
|
|
||||||
texture_width=2048,
|
|
||||||
texture_height=2048,
|
|
||||||
output_path="//finger_texture.png"
|
|
||||||
):
|
|
||||||
scene = bpy.context.scene
|
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
||||||
|
|
||||||
if obj_name:
|
|
||||||
obj = bpy.data.objects[obj_name]
|
|
||||||
else:
|
|
||||||
obj = bpy.context.active_object
|
|
||||||
if obj is None or obj.type != 'MESH':
|
|
||||||
raise ValueError("No valid mesh object selected")
|
|
||||||
|
|
||||||
cam = bpy.data.objects[camera_name]
|
|
||||||
if cam is None or cam.type != 'CAMERA':
|
|
||||||
raise ValueError("Camera not found")
|
|
||||||
|
|
||||||
if not obj.data.uv_layers.active:
|
|
||||||
obj.data.uv_layers.new(name="UVMap")
|
|
||||||
|
|
||||||
render = scene.render
|
|
||||||
res_x = render.resolution_x
|
|
||||||
res_y = render.resolution_y
|
|
||||||
|
|
||||||
cam_data = cam.data
|
|
||||||
if cam_data.sensor_fit == 'HORIZONTAL':
|
|
||||||
sensor_width = cam_data.sensor_width
|
|
||||||
sensor_height = sensor_width / (res_x / res_y)
|
|
||||||
else:
|
|
||||||
sensor_height = cam_data.sensor_height
|
|
||||||
sensor_width = sensor_height * (res_x / res_y)
|
|
||||||
focal = cam_data.lens
|
|
||||||
frame_width = sensor_width / focal
|
|
||||||
frame_height = sensor_height / focal
|
|
||||||
|
|
||||||
patch_uv_polygons = []
|
|
||||||
patch_colors = np.zeros((ny, nx, 3), dtype=np.float32)
|
|
||||||
|
|
||||||
for j in range(ny):
|
|
||||||
v_start = j / ny
|
|
||||||
v_end = (j+1) / ny
|
|
||||||
for i in range(nx):
|
|
||||||
u_start = i / nx
|
|
||||||
u_end = (i+1) / nx
|
|
||||||
|
|
||||||
corners_ndc = [
|
|
||||||
(u_start, v_start),
|
|
||||||
(u_end, v_start),
|
|
||||||
(u_end, v_end),
|
|
||||||
(u_start, v_end)
|
|
||||||
]
|
|
||||||
|
|
||||||
uv_poly = []
|
|
||||||
for (u_ndc, v_ndc) in corners_ndc:
|
|
||||||
sx = (u_ndc - 0.5) * frame_width
|
|
||||||
sy = (v_ndc - 0.5) * frame_height
|
|
||||||
direction_cam = Vector((sx, sy, -1.0))
|
|
||||||
direction_world = cam.matrix_world.to_quaternion() @ direction_cam
|
|
||||||
direction_world.normalize()
|
|
||||||
origin = cam.matrix_world.translation
|
|
||||||
|
|
||||||
hit, location, normal, face_index, hit_obj, matrix = scene.ray_cast(depsgraph, origin, direction_world)
|
|
||||||
if hit and hit_obj == obj:
|
|
||||||
uv = get_uv_from_face(obj, face_index, location)
|
|
||||||
if uv is not None:
|
|
||||||
uv_poly.append(uv)
|
|
||||||
|
|
||||||
if len(uv_poly) < 3:
|
|
||||||
patch_uv_polygons.append(None)
|
|
||||||
continue
|
|
||||||
patch_uv_polygons.append(uv_poly)
|
|
||||||
|
|
||||||
neighbors = []
|
|
||||||
if j > 0:
|
|
||||||
neighbors.append(patch_colors[j-1, i])
|
|
||||||
if i > 0:
|
|
||||||
neighbors.append(patch_colors[j, i-1])
|
|
||||||
|
|
||||||
if len(neighbors) == 0:
|
|
||||||
color = np.random.rand(3)
|
|
||||||
else:
|
|
||||||
color = generate_valid_color(neighbors, r)
|
|
||||||
patch_colors[j, i] = color
|
|
||||||
|
|
||||||
img_name = "FingerprintTexture"
|
|
||||||
if img_name in bpy.data.images:
|
|
||||||
bpy.data.images.remove(bpy.data.images[img_name])
|
|
||||||
image = bpy.data.images.new(img_name, width=texture_width, height=texture_height, alpha=True)
|
|
||||||
pixels = list(image.pixels)
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
for j in range(ny):
|
|
||||||
for i in range(nx):
|
|
||||||
uv_poly = patch_uv_polygons[idx]
|
|
||||||
idx += 1
|
|
||||||
if uv_poly is None:
|
|
||||||
continue
|
|
||||||
pixel_poly = [(u * (texture_width - 1), v * (texture_height - 1)) for (u, v) in uv_poly]
|
|
||||||
color = patch_colors[j, i]
|
|
||||||
rgba = (color[0], color[1], color[2], 1.0)
|
|
||||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
|
||||||
|
|
||||||
image.pixels[:] = pixels
|
|
||||||
image.update()
|
|
||||||
|
|
||||||
abs_path = bpy.path.abspath(output_path)
|
|
||||||
image.filepath_raw = abs_path
|
|
||||||
image.file_format = 'PNG'
|
|
||||||
image.save()
|
|
||||||
print(f"Texture saved to {abs_path}")
|
|
||||||
return image
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Run
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
if __name__ == "__main__":
|
|
||||||
generate_fingerprint_texture(
|
|
||||||
obj_name="FingerMesh", # Change to your mesh name
|
|
||||||
nx=50, ny=40,
|
|
||||||
r=0.05,
|
|
||||||
texture_width=4096,
|
|
||||||
texture_height=4096,
|
|
||||||
output_path="//finger_texture.png"
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user