Skip to content

Commit 8f0a11f

Browse files
authored
Merge pull request #1289 from maharshi-gor/polyxios-integration
NF: Polyxios Integration.
2 parents be8e2c1 + 2bf6c84 commit 8f0a11f

6 files changed

Lines changed: 425 additions & 6 deletions

File tree

docs/examples/_valid_examples.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ files = [
4444
"viz_contour.py",
4545
"viz_brownian_motion.py",
4646
"viz_network.py",
47+
"viz_read_mesh.py",
4748
]
4849

4950
[ui]

docs/examples/viz_read_mesh.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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()

fury/io.py

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
"""I/O functions for loading and saving images, textures."""
22

33
import os
4-
5-
# from tempfile import TemporaryDirectory as InTemporaryDirectory
64
from urllib.request import urlretrieve
75

86
# import warnings
97
from PIL import Image
108
import numpy as np
9+
import polyxios as px
1110

12-
# from fury.decorators import warn_on_args_to_kwargs
1311
from fury.lib import Texture, wgpu
14-
15-
# from fury.utils import set_input
1612
from fury.network.parser import parse_network, stringify_network
1713

1814

@@ -341,6 +337,121 @@ def save_network(network_data, file_path, format=None):
341337
f.write(data)
342338

343339

340+
def read_mesh(file_path, *, format=None):
341+
"""
342+
Read a mesh from a file using polyxios.
343+
344+
Parameters
345+
----------
346+
file_path : str
347+
The path to the mesh file.
348+
format : str, optional
349+
The specific file format override (e.g. '.vtk'). Inferred from the file
350+
extension when None.
351+
352+
Returns
353+
-------
354+
tuple
355+
A tuple containing:
356+
357+
- vertices (np.ndarray): Shape (N, 3) float32 array of vertex positions.
358+
- faces (np.ndarray or None): Shape (M, 3) int32 array of triangle face
359+
indices, or None when the mesh has no surface elements.
360+
- colors (np.ndarray or None): Shape (N, 3) float32 array of per-vertex
361+
RGB colors in [0, 1], or None when no vertex colors are present.
362+
"""
363+
poly = px.read(file_path, fmt=format)
364+
365+
vertices = np.asarray(poly.vertices, dtype=np.float32)
366+
367+
faces = poly.faces
368+
if faces is not None:
369+
faces = np.asarray(faces, dtype=np.int32)
370+
371+
colors = px.transforms.vertex_colors(poly)
372+
if colors is not None:
373+
colors = np.asarray(colors, dtype=np.float32)
374+
375+
return vertices, faces, colors
376+
377+
378+
def read_points(file_path, *, format=None):
379+
"""
380+
Read point coordinates from a file using polyxios.
381+
382+
Parameters
383+
----------
384+
file_path : str
385+
The path to the mesh file.
386+
format : str, optional
387+
The specific file format override (e.g. '.vtk'). Inferred from the file
388+
extension when None.
389+
390+
Returns
391+
-------
392+
tuple
393+
A tuple containing:
394+
395+
- points (np.ndarray): Shape (N, 3) float32 array of point positions.
396+
- colors (np.ndarray or None): Shape (N, 3) float32 array of per-point
397+
RGB colors in [0, 1], or None when no vertex colors are present.
398+
"""
399+
poly = px.read(file_path, fmt=format)
400+
401+
points = np.asarray(poly.vertices, dtype=np.float32)
402+
403+
colors = px.transforms.vertex_colors(poly)
404+
if colors is not None:
405+
colors = np.asarray(colors, dtype=np.float32)
406+
407+
return points, colors
408+
409+
410+
def read_lines(file_path, *, format=None):
411+
"""
412+
Read line segments from a file using polyxios.
413+
414+
Each line or poly_line element is translated into an array of its vertex
415+
positions, ready to be consumed by FURY line actors.
416+
417+
Parameters
418+
----------
419+
file_path : str
420+
The path to the mesh file.
421+
format : str, optional
422+
The specific file format override (e.g. '.vtk'). Inferred from the file
423+
extension when None.
424+
425+
Returns
426+
-------
427+
tuple
428+
A tuple containing:
429+
430+
- lines (list of np.ndarray): One Shape (P, 3) float32 array of vertex
431+
positions per line. Empty list when the file has no line elements.
432+
- colors (list of np.ndarray or None): One Shape (P, 3) float32 array of
433+
per-vertex RGB colors in [0, 1] per line, or None when no vertex
434+
colors are present.
435+
"""
436+
poly = px.read(file_path, fmt=format)
437+
438+
line_indices = poly.lines
439+
if line_indices is None:
440+
return [], None
441+
442+
vertices = np.asarray(poly.vertices, dtype=np.float32)
443+
lines = [vertices[idx] for idx in line_indices]
444+
445+
vertex_colors = px.transforms.vertex_colors(poly)
446+
if vertex_colors is None:
447+
colors = None
448+
else:
449+
vertex_colors = np.asarray(vertex_colors, dtype=np.float32)
450+
colors = [vertex_colors[idx] for idx in line_indices]
451+
452+
return lines, colors
453+
454+
344455
# def load_polydata(file_name):
345456
# """Load a vtk polydata to a supported format file.
346457

0 commit comments

Comments
 (0)