Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/examples/_valid_examples.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ files = [
"viz_contour.py",
"viz_brownian_motion.py",
"viz_network.py",
"viz_read_mesh.py",
]

[ui]
Expand Down
224 changes: 224 additions & 0 deletions docs/examples/viz_read_mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""
=================================
Reading Meshes, Lines, and Points
=================================

This example demonstrates how to fetch 3D data assets from the
``polyxios-data`` repository, read them into NumPy arrays using FURY's
dedicated I/O methods, and render them as distinct colored actors side by side:
a surface mesh, a collection of lines, and a point cloud.

Polyxios is the lightweight, dependency-free I/O backend used by FURY. It
is designed to auto-detect file formats by extension and map complex scientific
surface structures (``.vtk``, ``.vtp``, ``.ply``, ``.obj``, ``.mesh``)
seamlessly into memory-contiguous NumPy arrays, and its ``fetch`` helper downloads
and caches sample assets so tutorials and tests can run without bundling large files.

This example highlights three fundamental data archetypes:

* **Surface Meshes (via ``read_mesh``):** Returns a triple of
``(vertices, faces, colors)`` where ``vertices`` is an ``(N, 3)`` float32 array,
``faces`` is an ``(M, 3)`` int32 array of triangulated indices, and ``colors`` is an
optional ``(N, 3)`` float32 color map.

* **Line Streams (via ``read_lines``):** Returns a tuple of ``(lines, colors)``. Here,
``lines`` is a list of independent ``(P, 3)`` float32 arrays (where each array
represents an individual continuous stroke or fiber pathway), and ``colors`` contains
a corresponding list of per-vertex color maps.

* **Point Clouds (via ``read_points``):** Returns a tuple of ``(points, colors)`` where
``points`` is an ``(N, 3)`` float32 coordinate block representing un-connected point
data, and ``colors`` is an optional per-point attribute array.
"""

#########################################################################################
# Import the required libraries.
import numpy as np
import polyxios as px

from fury import actor, ui, window
from fury.io import read_lines, read_mesh, read_points

#########################################################################################
# Define helper to process geometry, normalize space, and apply fallback gradients.


def points_to_actor(file_path):
"""Read a point cloud file and build a colored point actor."""
points, colors = read_points(file_path)

if points.size == 0:
raise ValueError(f"No points found in file: {file_path}")

# Center on the origin and normalize the scale to roughly unit size
points = points - points.mean(axis=0)
points = (points / np.max(np.abs(points))).astype(np.float32)

# Use the file's colors when present, otherwise apply a height-based gradient
if colors is None:
height = points[:, 1]
height_range = height.max() - height.min()
y_range = height_range if height_range > 0 else 1.0

t = ((height - height.min()) / y_range)[:, None]
low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32)
high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32)
colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32)

# ``actor.point`` turns an (N, 3) array of point positions and their
# matching per-point colors into an optimized graphic object.
point_actor = actor.point(points, colors=colors)
return point_actor, len(points)


def lines_to_actor(file_path):
"""Read a lines file and build a colored stream/line actor."""
lines, colors = read_lines(file_path)

if not lines:
raise ValueError(f"No line elements found in file: {file_path}")

# Stack all points temporarily to calculate overall centering and scaling metrics
all_points = np.vstack(lines)
center = all_points.mean(axis=0)
max_scale = np.max(np.abs(all_points - center))

# Center on the origin and normalize the scale to roughly unit size
normalized_lines = [
((line - center) / max_scale).astype(np.float32) for line in lines
]

# Use the file's colors when present, otherwise apply a height-based gradient
if colors is None:
# Re-stack lines to compute a global bounding box for the height gradient
all_norm_points = np.vstack(normalized_lines)
y_min = all_norm_points[:, 1].min()
y_max = all_norm_points[:, 1].max()
y_range = y_max - y_min if (y_max - y_min) > 0 else 1.0

low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32)
high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32)

colors = []
for line in normalized_lines:
heights = line[:, 1]
t = ((heights - y_min) / y_range)[:, None]
line_colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32)
colors.append(line_colors)

# ``actor.streamlines`` accepts a list of coordinate arrays and color arrays
line_actor = actor.streamlines(normalized_lines, colors=colors)

# Calculate totals for reporting
total_vertices = sum(len(line) for line in lines)
total_lines = len(lines)

return line_actor, total_vertices, total_lines


def mesh_to_actor(file_path):
"""Read a mesh file and build a colored surface actor."""
vertices, faces, colors = read_mesh(file_path)

# Center on the origin and normalize the scale to roughly unit size.
vertices = vertices - vertices.mean(axis=0)
vertices = (vertices / np.max(np.abs(vertices))).astype(np.float32)

# Use the file's colors when present, otherwise a height-based gradient.
if colors is None:
height = vertices[:, 1]
t = ((height - height.min()) / (height.max() - height.min()))[:, None]
low_color = np.array([0.10, 0.20, 0.70], dtype=np.float32)
high_color = np.array([0.95, 0.75, 0.20], dtype=np.float32)
colors = (low_color * (1.0 - t) + high_color * t).astype(np.float32)

# ``actor.surface`` wraps the geometry, material and mesh creation for us,
# turning the vertices, faces and per-vertex colors into a ready actor.
surf = actor.surface(vertices, faces, colors=colors)
return surf, len(vertices), len(faces)


#########################################################################################
# Fetch the sample data assets via polyxios.
#
# * ``Human.vtp`` a surface mesh possessing native topological faces and color tracks.
# * ``hello.vtk`` contains line elements forming spatial lettering out of poly-line.
# * ``star.mesh`` handles plain point coordinates lacking explicitly structured links.
human_path = px.fetch("Human.vtp")
line_path = px.fetch("hello.vtk")
ball_path = px.fetch("star.mesh")

# Generate the actors and fetch metadata counts
mesh_actor, mesh_nv, mesh_nf = mesh_to_actor(human_path)
line_actor, line_nv, line_nl = lines_to_actor(line_path)
point_actor, point_nv = points_to_actor(ball_path)

print(f"Human.vtp (Mesh): {mesh_nv} vertices, {mesh_nf} faces")
print(f"hello.vtk (Lines): {line_nv} vertices across {line_nl} lines")
print(f"star.mesh (Points): {point_nv} points")

#########################################################################################
# Coordinate alignment transforms.
# ``Human.vtp`` is modeled lying along its Z axis, so by default it faces the
# camera end-on. Every FURY actor exposes transform helpers (``rotate``,
# ``translate``, ``scale``), so we stand it upright with a -90 degrees rotation
# about the X axis, mapping its head-to-toe axis to the vertical and apply 180 degrees
# so it faces the camera.
mesh_actor.rotate((-90, 180, 0))

#########################################################################################
# Place the three actors side by side (Left, Center, Right) to avoid overlapping.
mesh_actor.local.position = (-3.0, 0.0, 0.0)
line_actor.local.position = (0.0, 0.0, 0.0)
point_actor.local.position = (3.0, 0.0, 0.0)

#########################################################################################
# Add matching 3D text labels directly beneath each of the three objects.
labels_actor = actor.text(
["Mesh (Human.vtp)", "Lines (hello.vtk)", "Points (star.mesh)"],
position=[(-3.0, -1.25, 0.0), (0.0, -1.25, 0.0), (3.0, -1.25, 0.0)],
colors=(0.9, 0.9, 0.95),
font_size=0.16,
anchor="top-center",
)

#########################################################################################
# Set up the 3D scene and register all visual elements.
scene = window.Scene(background=(0.05, 0.05, 0.08))
scene.add(mesh_actor)
scene.add(line_actor)
scene.add(point_actor)
scene.add(labels_actor)

#########################################################################################
# Add a 2D text HUD overlay detailing what each object represents.
info_text = (
f"FURY x Polyxios Geometry Reader\n"
f"Left: Human.vtp (Mesh: {mesh_nv} verts, {mesh_nf} faces)\n"
f"Center: hello.vtk (Lines: {line_nv} total vertices, {line_nl} lines)\n"
f"Right: star.mesh (Points: {point_nv} coordinates)\n"
f"Decoupled data paths processed into native NumPy arrays."
)

hud_label = ui.TextBlock2D(
text=info_text,
position=(20, 20),
font_size=16,
color=(0.9, 0.9, 0.95),
bold=False,
dynamic_bbox=True,
)
scene.add(hud_label)

#########################################################################################
# Initialize the ShowManager, frame the camera to fit the wider 3-element layout,
# and initialize the rendering window loop.
show_m = window.ShowManager(
scene=scene, size=(1280, 728), title="FURY Multimodal Reader"
)

camera = show_m.screens[0].camera
camera.local.position = (0.0, -0.15, 6.0)
camera.look_at((0.0, -0.15, 0.0))

show_m.start()
121 changes: 116 additions & 5 deletions fury/io.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
"""I/O functions for loading and saving images, textures."""

import os

# from tempfile import TemporaryDirectory as InTemporaryDirectory
from urllib.request import urlretrieve

# import warnings
from PIL import Image
import numpy as np
import polyxios as px

# from fury.decorators import warn_on_args_to_kwargs
from fury.lib import Texture, wgpu

# from fury.utils import set_input
from fury.network.parser import parse_network, stringify_network


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


def read_mesh(file_path, *, format=None):
"""
Read a mesh from a file using polyxios.

Parameters
----------
file_path : str
The path to the mesh file.
format : str, optional
The specific file format override (e.g. '.vtk'). Inferred from the file
extension when None.

Returns
-------
tuple
A tuple containing:

- vertices (np.ndarray): Shape (N, 3) float32 array of vertex positions.
- faces (np.ndarray or None): Shape (M, 3) int32 array of triangle face
indices, or None when the mesh has no surface elements.
- colors (np.ndarray or None): Shape (N, 3) float32 array of per-vertex
RGB colors in [0, 1], or None when no vertex colors are present.
"""
poly = px.read(file_path, fmt=format)

vertices = np.asarray(poly.vertices, dtype=np.float32)

faces = poly.faces
if faces is not None:
faces = np.asarray(faces, dtype=np.int32)

colors = px.transforms.vertex_colors(poly)
if colors is not None:
colors = np.asarray(colors, dtype=np.float32)

return vertices, faces, colors


def read_points(file_path, *, format=None):
"""
Read point coordinates from a file using polyxios.

Parameters
----------
file_path : str
The path to the mesh file.
format : str, optional
The specific file format override (e.g. '.vtk'). Inferred from the file
extension when None.

Returns
-------
tuple
A tuple containing:

- points (np.ndarray): Shape (N, 3) float32 array of point positions.
- colors (np.ndarray or None): Shape (N, 3) float32 array of per-point
RGB colors in [0, 1], or None when no vertex colors are present.
"""
poly = px.read(file_path, fmt=format)

points = np.asarray(poly.vertices, dtype=np.float32)

colors = px.transforms.vertex_colors(poly)
if colors is not None:
colors = np.asarray(colors, dtype=np.float32)

return points, colors


def read_lines(file_path, *, format=None):
"""
Read line segments from a file using polyxios.

Each line or poly_line element is translated into an array of its vertex
positions, ready to be consumed by FURY line actors.

Parameters
----------
file_path : str
The path to the mesh file.
format : str, optional
The specific file format override (e.g. '.vtk'). Inferred from the file
extension when None.

Returns
-------
tuple
A tuple containing:

- lines (list of np.ndarray): One Shape (P, 3) float32 array of vertex
positions per line. Empty list when the file has no line elements.
- colors (list of np.ndarray or None): One Shape (P, 3) float32 array of
per-vertex RGB colors in [0, 1] per line, or None when no vertex
colors are present.
"""
poly = px.read(file_path, fmt=format)

line_indices = poly.lines
if line_indices is None:
return [], None

vertices = np.asarray(poly.vertices, dtype=np.float32)
lines = [vertices[idx] for idx in line_indices]

vertex_colors = px.transforms.vertex_colors(poly)
if vertex_colors is None:
colors = None
else:
vertex_colors = np.asarray(vertex_colors, dtype=np.float32)
colors = [vertex_colors[idx] for idx in line_indices]

return lines, colors


# def load_polydata(file_name):
# """Load a vtk polydata to a supported format file.

Expand Down
Loading
Loading