diff --git a/examples/plot_data.py b/examples/plot_data.py index 49e6fd4..65aa22c 100644 --- a/examples/plot_data.py +++ b/examples/plot_data.py @@ -23,6 +23,37 @@ except: print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.") 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(): p1 = win.addPlot(title="法向力") 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]) start_time = None # Will be set when sensor starts 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 + data_filtered = data.copy() for i in range(3): data[:-1, 0, i] = data[1:, 0, i] # 时间轴左移 data[:-1, 1, i] = data[1:, 1, i] # 力数据左移 @@ -171,7 +203,11 @@ def main(): data[-1, 1, 0] = fn data[-1, 1, 1] = fx 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) if key & 0xFF == ord("q"): diff --git a/tools/3d_patch_generation_blender/checker_patch_dd02c.py b/tools/3d_patch_generation_blender/checker_patch_dd02c.py new file mode 100644 index 0000000..f8d0aa8 --- /dev/null +++ b/tools/3d_patch_generation_blender/checker_patch_dd02c.py @@ -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() \ No newline at end of file diff --git a/tools/3d_patch_generation_blender/dd02c.blend b/tools/3d_patch_generation_blender/dd02c.blend new file mode 100644 index 0000000..119edbd Binary files /dev/null and b/tools/3d_patch_generation_blender/dd02c.blend differ diff --git a/tools/3d_patch_generation_blender/random_patch_dd02c.py b/tools/3d_patch_generation_blender/random_patch_dd02c.py new file mode 100644 index 0000000..a010717 --- /dev/null +++ b/tools/3d_patch_generation_blender/random_patch_dd02c.py @@ -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() \ No newline at end of file diff --git a/tools/3d_patch_generation_blender/test_blender.py b/tools/3d_patch_generation_blender/test_blender.py deleted file mode 100644 index 7e2f097..0000000 --- a/tools/3d_patch_generation_blender/test_blender.py +++ /dev/null @@ -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" - ) \ No newline at end of file