|
| 1 | +""" |
| 2 | +================================= |
| 3 | +Reading Meshes, Lines, and Points |
| 4 | +================================= |
| 5 | +
|
| 6 | +This example demonstrates how to fetch 3D data assets from the |
| 7 | +``polyxios-data`` repository, read them into NumPy arrays using FURY's |
| 8 | +dedicated I/O methods, and render them as distinct colored actors side by side: |
| 9 | +a surface mesh, a collection of lines, and a point cloud. |
| 10 | +
|
| 11 | +Polyxios is the lightweight, dependency-free I/O backend used by FURY. It |
| 12 | +is designed to auto-detect file formats by extension and map complex scientific |
| 13 | +surface structures (``.vtk``, ``.vtp``, ``.ply``, ``.obj``, ``.mesh``) |
| 14 | +seamlessly into memory-contiguous NumPy arrays, and its ``fetch`` helper downloads |
| 15 | +and caches sample assets so tutorials and tests can run without bundling large files. |
| 16 | +
|
| 17 | +This example highlights three fundamental data archetypes: |
| 18 | +
|
| 19 | +* **Surface Meshes (via ``read_mesh``):** Returns a triple of |
| 20 | + ``(vertices, faces, colors)`` where ``vertices`` is an ``(N, 3)`` float32 array, |
| 21 | + ``faces`` is an ``(M, 3)`` int32 array of triangulated indices, and ``colors`` is an |
| 22 | + optional ``(N, 3)`` float32 color map. |
| 23 | +
|
| 24 | +* **Line Streams (via ``read_lines``):** Returns a tuple of ``(lines, colors)``. Here, |
| 25 | + ``lines`` is a list of independent ``(P, 3)`` float32 arrays (where each array |
| 26 | + represents an individual continuous stroke or fiber pathway), and ``colors`` contains |
| 27 | + a corresponding list of per-vertex color maps. |
| 28 | +
|
| 29 | +* **Point Clouds (via ``read_points``):** Returns a tuple of ``(points, colors)`` where |
| 30 | + ``points`` is an ``(N, 3)`` float32 coordinate block representing un-connected point |
| 31 | + data, and ``colors`` is an optional per-point attribute array. |
| 32 | +""" |
| 33 | + |
| 34 | +######################################################################################### |
| 35 | +# Import the required libraries. |
| 36 | +import numpy as np |
| 37 | +import polyxios as px |
| 38 | + |
| 39 | +from fury import actor, ui, window |
| 40 | +from fury.io import read_lines, read_mesh, read_points |
| 41 | + |
| 42 | +######################################################################################### |
| 43 | +# Define helper to process geometry, normalize space, and apply fallback gradients. |
| 44 | + |
| 45 | + |
| 46 | +def points_to_actor(file_path): |
| 47 | + """Read a point cloud file and build a colored point actor.""" |
| 48 | + points, colors = read_points(file_path) |
| 49 | + |
| 50 | + if points.size == 0: |
| 51 | + raise ValueError(f"No points found in file: {file_path}") |
| 52 | + |
| 53 | + # Center on the origin and normalize the scale to roughly unit size |
| 54 | + points = points - points.mean(axis=0) |
| 55 | + points = (points / np.max(np.abs(points))).astype(np.float32) |
| 56 | + |
| 57 | + # Use the file's colors when present, otherwise apply a height-based gradient |
| 58 | + if colors is None: |
| 59 | + height = points[:, 1] |
| 60 | + height_range = height.max() - height.min() |
| 61 | + y_range = height_range if height_range > 0 else 1.0 |
| 62 | + |
| 63 | + t = ((height - height.min()) / y_range)[:, None] |
| 64 | + low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32) |
| 65 | + high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32) |
| 66 | + colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32) |
| 67 | + |
| 68 | + # ``actor.point`` turns an (N, 3) array of point positions and their |
| 69 | + # matching per-point colors into an optimized graphic object. |
| 70 | + point_actor = actor.point(points, colors=colors) |
| 71 | + return point_actor, len(points) |
| 72 | + |
| 73 | + |
| 74 | +def lines_to_actor(file_path): |
| 75 | + """Read a lines file and build a colored stream/line actor.""" |
| 76 | + lines, colors = read_lines(file_path) |
| 77 | + |
| 78 | + if not lines: |
| 79 | + raise ValueError(f"No line elements found in file: {file_path}") |
| 80 | + |
| 81 | + # Stack all points temporarily to calculate overall centering and scaling metrics |
| 82 | + all_points = np.vstack(lines) |
| 83 | + center = all_points.mean(axis=0) |
| 84 | + max_scale = np.max(np.abs(all_points - center)) |
| 85 | + |
| 86 | + # Center on the origin and normalize the scale to roughly unit size |
| 87 | + normalized_lines = [ |
| 88 | + ((line - center) / max_scale).astype(np.float32) for line in lines |
| 89 | + ] |
| 90 | + |
| 91 | + # Use the file's colors when present, otherwise apply a height-based gradient |
| 92 | + if colors is None: |
| 93 | + # Re-stack lines to compute a global bounding box for the height gradient |
| 94 | + all_norm_points = np.vstack(normalized_lines) |
| 95 | + y_min = all_norm_points[:, 1].min() |
| 96 | + y_max = all_norm_points[:, 1].max() |
| 97 | + y_range = y_max - y_min if (y_max - y_min) > 0 else 1.0 |
| 98 | + |
| 99 | + low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32) |
| 100 | + high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32) |
| 101 | + |
| 102 | + colors = [] |
| 103 | + for line in normalized_lines: |
| 104 | + heights = line[:, 1] |
| 105 | + t = ((heights - y_min) / y_range)[:, None] |
| 106 | + line_colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32) |
| 107 | + colors.append(line_colors) |
| 108 | + |
| 109 | + # ``actor.streamlines`` accepts a list of coordinate arrays and color arrays |
| 110 | + line_actor = actor.streamlines(normalized_lines, colors=colors) |
| 111 | + |
| 112 | + # Calculate totals for reporting |
| 113 | + total_vertices = sum(len(line) for line in lines) |
| 114 | + total_lines = len(lines) |
| 115 | + |
| 116 | + return line_actor, total_vertices, total_lines |
| 117 | + |
| 118 | + |
| 119 | +def mesh_to_actor(file_path): |
| 120 | + """Read a mesh file and build a colored surface actor.""" |
| 121 | + vertices, faces, colors = read_mesh(file_path) |
| 122 | + |
| 123 | + # Center on the origin and normalize the scale to roughly unit size. |
| 124 | + vertices = vertices - vertices.mean(axis=0) |
| 125 | + vertices = (vertices / np.max(np.abs(vertices))).astype(np.float32) |
| 126 | + |
| 127 | + # Use the file's colors when present, otherwise a height-based gradient. |
| 128 | + if colors is None: |
| 129 | + height = vertices[:, 1] |
| 130 | + t = ((height - height.min()) / (height.max() - height.min()))[:, None] |
| 131 | + low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32) |
| 132 | + high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32) |
| 133 | + colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32) |
| 134 | + |
| 135 | + # ``actor.surface`` wraps the geometry, material and mesh creation for us, |
| 136 | + # turning the vertices, faces and per-vertex colors into a ready actor. |
| 137 | + surf = actor.surface(vertices, faces, colors=colors) |
| 138 | + return surf, len(vertices), len(faces) |
| 139 | + |
| 140 | + |
| 141 | +######################################################################################### |
| 142 | +# Fetch the sample data assets via polyxios. |
| 143 | +# |
| 144 | +# * ``Human.vtp`` a surface mesh possessing native topological faces and color tracks. |
| 145 | +# * ``hello.vtk`` contains line elements forming spatial lettering out of poly-line. |
| 146 | +# * ``star.mesh`` handles plain point coordinates lacking explicitly structured links. |
| 147 | +human_path = px.fetch("Human.vtp") |
| 148 | +line_path = px.fetch("hello.vtk") |
| 149 | +ball_path = px.fetch("star.mesh") |
| 150 | + |
| 151 | +# Generate the actors and fetch metadata counts |
| 152 | +mesh_actor, mesh_nv, mesh_nf = mesh_to_actor(human_path) |
| 153 | +line_actor, line_nv, line_nl = lines_to_actor(line_path) |
| 154 | +point_actor, point_nv = points_to_actor(ball_path) |
| 155 | + |
| 156 | +print(f"Human.vtp (Mesh): {mesh_nv} vertices, {mesh_nf} faces") |
| 157 | +print(f"hello.vtk (Lines): {line_nv} vertices across {line_nl} lines") |
| 158 | +print(f"star.mesh (Points): {point_nv} points") |
| 159 | + |
| 160 | +######################################################################################### |
| 161 | +# Coordinate alignment transforms. |
| 162 | +# ``Human.vtp`` is modeled lying along its Z axis, so by default it faces the |
| 163 | +# camera end-on. Every FURY actor exposes transform helpers (``rotate``, |
| 164 | +# ``translate``, ``scale``), so we stand it upright with a -90 degrees rotation |
| 165 | +# about the X axis, mapping its head-to-toe axis to the vertical and apply 180 degrees |
| 166 | +# so it faces the camera. |
| 167 | +mesh_actor.rotate((-90, 180, 0)) |
| 168 | + |
| 169 | +######################################################################################### |
| 170 | +# Place the three actors side by side (Left, Center, Right) to avoid overlapping. |
| 171 | +mesh_actor.local.position = (-3.0, 0.0, 0.0) |
| 172 | +line_actor.local.position = (0.0, 0.0, 0.0) |
| 173 | +point_actor.local.position = (3.0, 0.0, 0.0) |
| 174 | + |
| 175 | +######################################################################################### |
| 176 | +# Add matching 3D text labels directly beneath each of the three objects. |
| 177 | +labels_actor = actor.text( |
| 178 | + ["Mesh (Human.vtp)", "Lines (hello.vtk)", "Points (star.mesh)"], |
| 179 | + position=[(-3.0, -1.25, 0.0), (0.0, -1.25, 0.0), (3.0, -1.25, 0.0)], |
| 180 | + colors=(0.9, 0.9, 0.95), |
| 181 | + font_size=0.16, |
| 182 | + anchor="top-center", |
| 183 | +) |
| 184 | + |
| 185 | +######################################################################################### |
| 186 | +# Set up the 3D scene and register all visual elements. |
| 187 | +scene = window.Scene(background=(0.05, 0.05, 0.08)) |
| 188 | +scene.add(mesh_actor) |
| 189 | +scene.add(line_actor) |
| 190 | +scene.add(point_actor) |
| 191 | +scene.add(labels_actor) |
| 192 | + |
| 193 | +######################################################################################### |
| 194 | +# Add a 2D text HUD overlay detailing what each object represents. |
| 195 | +info_text = ( |
| 196 | + f"FURY x Polyxios Geometry Reader\n" |
| 197 | + f"Left: Human.vtp (Mesh: {mesh_nv} verts, {mesh_nf} faces)\n" |
| 198 | + f"Center: hello.vtk (Lines: {line_nv} total vertices, {line_nl} lines)\n" |
| 199 | + f"Right: star.mesh (Points: {point_nv} coordinates)\n" |
| 200 | + f"Decoupled data paths processed into native NumPy arrays." |
| 201 | +) |
| 202 | + |
| 203 | +hud_label = ui.TextBlock2D( |
| 204 | + text=info_text, |
| 205 | + position=(20, 20), |
| 206 | + font_size=16, |
| 207 | + color=(0.9, 0.9, 0.95), |
| 208 | + bold=False, |
| 209 | + dynamic_bbox=True, |
| 210 | +) |
| 211 | +scene.add(hud_label) |
| 212 | + |
| 213 | +######################################################################################### |
| 214 | +# Initialize the ShowManager, frame the camera to fit the wider 3-element layout, |
| 215 | +# and initialize the rendering window loop. |
| 216 | +show_m = window.ShowManager( |
| 217 | + scene=scene, size=(1280, 728), title="FURY Multimodal Reader" |
| 218 | +) |
| 219 | + |
| 220 | +camera = show_m.screens[0].camera |
| 221 | +camera.local.position = (0.0, -0.15, 6.0) |
| 222 | +camera.look_at((0.0, -0.15, 0.0)) |
| 223 | + |
| 224 | +show_m.start() |
0 commit comments