import vtk
from vtk.util.numpy_support import numpy_to_vtk, numpy_to_vtkIdTypeArray, vtk_to_numpy
import numpy as np
import os
import logging
from meshparty.utils import remove_unused_verts
[docs]def numpy_to_vtk_cells(mat):
"""function to convert a numpy array of integers to a vtkCellArray
Parameters
----------
mat : np.array
MxN array to be converted
Returns
-------
vtk.vtkCellArray
representing the numpy array, has the same shaped cell (N) at each of the M indices
"""
cells = vtk.vtkCellArray()
# Seemingly, VTK may be compiled as 32 bit or 64 bit.
# We need to make sure that we convert the trilist to the correct dtype
# based on this. See numpy_to_vtkIdTypeArray() for details.
isize = vtk.vtkIdTypeArray().GetDataTypeSize()
req_dtype = np.int32 if isize == 4 else np.int64
n_elems = mat.shape[0]
n_dim = mat.shape[1]
cells.SetCells(n_elems,
numpy_to_vtkIdTypeArray(
np.hstack((np.ones(n_elems)[:, None] * n_dim,
mat)).astype(req_dtype).ravel(),
deep=1))
return cells
[docs]def numpy_rep_to_vtk(vertices, shapes, edges=None):
""" converts a numpy representation of vertices and vertex connection graph
to a polydata object and corresponding cell array
Parameters
----------
vertices: a Nx3 numpy array of vertex locations
shapes: a MxK numpy array of vertex connectivity
(could be triangles (K=3) or edges (K=2))
Returns
-------
vtk.vtkPolyData
a polydata object with point set according to vertices,
vtkCellArray
a vtkCellArray of the shapes
"""
mesh = vtk.vtkPolyData()
points = vtk.vtkPoints()
points.SetData(numpy_to_vtk(vertices, deep=1))
mesh.SetPoints(points)
cells = numpy_to_vtk_cells(shapes)
if edges is not None:
if len(edges) > 0:
edges = numpy_to_vtk_cells(edges)
else:
edges = None
return mesh, cells, edges
[docs]def graph_to_vtk(vertices, edges):
""" converts a numpy representation of vertices and edges
to a vtkPolyData object
Parameters
----------
vertices: np.array
a Nx3 numpy array of vertex locations
edges: np.array
a Mx2 numpy array of vertex connectivity
where the values are the indexes of connected vertices
Returns
-------
vtk.vtkPolyData
a polydata object with point set according to vertices
and edges as its Lines
Raises
------
ValueError
if edges is not 2d or refers to out of bounds vertices
"""
if edges.shape[1] != 2:
raise ValueError('graph_to_vtk() only works on edge lists')
if np.max(edges) >= len(vertices):
msg = 'edges refer to non existent vertices {}.'
raise ValueError(msg.format(np.max(edges)))
mesh, cells, edges = numpy_rep_to_vtk(vertices, edges)
mesh.SetLines(cells)
return mesh
[docs]def trimesh_to_vtk(vertices, tris, graph_edges=None):
"""Return a `vtkPolyData` representation of a :obj:`TriMesh` instance
Parameters
----------
vertices : np.array
numpy array of Nx3 vertex positions (x,y,z)
tris: np.array
numpy array of Mx3 triangle vertex indices (int64)
graph_edges: np.array
numpy array of Kx2 of edges to set as the vtkPolyData.Lines
Returns
-------
vtk_mesh : vtk.vtkPolyData
A VTK mesh representation of the mesh :obj:`trimesh.TriMesh` data
Raises
------
ValueError:
If the input trimesh is not 3D
or tris refers to out of bounds vertex indices
"""
if tris.shape[1] != 3:
raise ValueError('trimesh_to_vtk() only works on 3D TriMesh instances')
if np.max(tris) >= len(vertices):
msg = 'edges refer to non existent vertices {}.'
raise ValueError(msg.format(np.max(tris)))
mesh, cells, edges = numpy_rep_to_vtk(vertices, tris, graph_edges)
mesh.SetPolys(cells)
if edges is not None:
mesh.SetLines(edges)
return mesh
[docs]def vtk_cellarray_to_shape(vtk_cellarray, ncells):
"""Turn a vtkCellArray into a numpyarray of a fixed shape
assumes your cell array has uniformed sized cells
Parameters
----------
vtk_cellarray : vtk.vtkCellArray
a cell array to convert
ncells: int
how many cells are in array
Returns
-------
np.array
cellarray, a ncells x K array of cells, where K is the
uniform shape of the cells. Will error if cells are not uniform
"""
cellarray = vtk_to_numpy(vtk_cellarray)
cellarray = cellarray.reshape(ncells, int(len(cellarray)/ncells))
return cellarray[:, 1:]
[docs]def decimate_trimesh(trimesh, reduction=.1):
""" routine to decimate a mesh through vtk
Parameters
----------
trimesh : trimesh_io.Mesh
a mesh to decimate
reduction: float
factor to decimate (default .1)
Returns
-------
np.array
points, the Nx3 mesh of vertices
np.array
tris, the Kx3 indices of faces
"""
poly = trimesh_to_vtk(trimesh.vertices, trimesh.faces)
dec = vtk.vtkDecimatePro()
dec.SetTargetReduction(reduction)
dec.PreserveTopologyOn()
dec.SetInputData(poly)
dec.Update()
out_poly = dec.GetOutput()
points = vtk_to_numpy(out_poly.GetPoints().GetData())
ntris = out_poly.GetNumberOfPolys()
tris = vtk_cellarray_to_shape(out_poly.GetPolys().GetData(), ntris)
return points, tris
[docs]def poly_to_mesh_components(poly):
""" converts a vtkPolyData to its numpy components
Parameters
----------
poly : vtk.vtkPolyData
a polydate object to convert to numpy components
Returns
-------
np.array
points, the Nx3 set of vertex locations
np.array
tris, the KxD set of faces (assumes a uniform cellarray)
np.array
edges, if exists uses the GetLines to make edges
"""
points = vtk_to_numpy(poly.GetPoints().GetData())
ntris = poly.GetNumberOfPolys()
if ntris > 0:
tris = vtk_cellarray_to_shape(poly.GetPolys().GetData(), ntris)
else:
tris = None
nedges = poly.GetNumberOfLines()
if nedges > 0:
edges = vtk_cellarray_to_shape(poly.GetLines().GetData(), nedges)
else:
edges = None
return points, tris, edges
[docs]def render_actors(actors, camera=None, do_save=False, filename=None,
scale=4, back_color=(1, 1, 1),
VIDEO_WIDTH=None, VIDEO_HEIGHT=None,
video_width=1080, video_height=720,
return_keyframes=False):
"""
Visualize a set of actors in a 3d scene, optionally saving a snapshot.
Creates a window, renderer, interactor, add the actors and starts the visualization
(can save images and close render window)
Parameters
----------
actors : list[vtkActor]
list of actors to render (see mesh_actor, point_cloud_actor, skeleton_actor)
camera : :obj:`vtkCamera`
camera to use for scence (optional..default to fit scene)
do_save: bool
write png image to disk, if false will open interactive window (default False)
filename: str
filepath to save png image to (default None)
scale:
scale factor to use when saving images to disk (default 4) for higher res images
back_color: Iterable
rgb values (0,1) to determine for background color (default 1,1,1 = white)
return_keyframes : bool
whether to save a new camera as a keyframes when you press 'k' with window open
Returns
-------
:obj:`vtk.vtkRenderer`
renderer when code was finished
(useful for retrieving user input camera position ren.GetActiveCamera())
(list[vtk.vtkCamera])
list of vtk cameras when user pressed 'k' (only if return_keyframes=True)
"""
if do_save:
assert(filename is not None)
if VIDEO_HEIGHT is not None:
logging.warning('VIDEO_HEIGHT deprecated, please use video_height')
video_height=VIDEO_HEIGHT
if VIDEO_WIDTH is not None:
logging.warning('VIDEO_WIDTH is deprecated, please use VIDEO_WIDTH')
video_width=VIDEO_WIDTH
print('setting up renderer')
# create a rendering window and renderer
ren, renWin, iren = _setup_renderer(
video_width, video_height, back_color, camera=camera)
print('done setting up')
for a in actors:
# assign actor to the renderer
ren.AddActor(a)
print('actors added')
# render
if camera is None:
ren.ResetCamera()
else:
ren.ResetCameraClippingRange()
camera.ViewingRaysModified()
print('camera set')
if return_keyframes:
key_frame_cameras = []
def vtkKeyPress(obj, event):
key = obj.GetKeySym()
if key == 'k':
key_camera = vtk.vtkCamera()
key_camera.DeepCopy(ren.GetActiveCamera())
key_frame_cameras.append(key_camera)
return
iren.AddObserver("KeyPressEvent", vtkKeyPress)
renWin.Render()
print('render done')
if do_save is False:
trackCamera = vtk.vtkInteractorStyleTrackballCamera()
iren.SetInteractorStyle(trackCamera)
# enable user interface interactor
iren.Initialize()
iren.Render()
iren.Start()
if do_save is True:
renWin.OffScreenRenderingOn()
w2if = vtk.vtkWindowToImageFilter()
w2if.SetScale(scale)
w2if.SetInput(renWin)
w2if.Update()
writer = vtk.vtkPNGWriter()
writer.SetFileName(filename)
writer.SetInputData(w2if.GetOutput())
writer.Write()
print('finalizing..')
renWin.Finalize()
if return_keyframes:
return ren, key_frame_cameras
else:
return ren
[docs]def camera_from_quat(pos_nm, orient_quat, camera_distance=10000, ngl_correct=True):
"""define a vtk camera with a particular orientation
Parameters
----------
pos_nm: np.array, list, tuple
an iterator of length 3 containing the focus point of the camera
orient_quat: np.array, list, tuple
a len(4) quatenerion (x,y,z,w) describing the rotation of the camera
such as returned by neuroglancer x,y,z,w all in [0,1] range
camera_distance: float
the desired distance from pos_nm to the camera (default = 10000 nm)
Returns
-------
vtk.vtkCamera
a vtk camera setup according to these rules
"""
camera = vtk.vtkCamera()
# define the quaternion in vtk, note the swapped order
# w,x,y,z instead of x,y,z,w
quat_vtk = vtk.vtkQuaterniond(orient_quat[3],
orient_quat[0],
orient_quat[1],
orient_quat[2])
# use this to define a rotation matrix in x,y,z
# right handed units
M = np.zeros((3, 3), dtype=np.float32)
quat_vtk.ToMatrix3x3(M)
# the default camera orientation is y up
up = [0, 1, 0]
# calculate default camera position is backed off in positive z
pos = [0, 0, camera_distance]
# set the camera rototation by applying the rotation matrix
camera.SetViewUp(*np.dot(M, up))
# set the camera position by applying the rotation matrix
camera.SetPosition(*np.dot(M, pos))
if ngl_correct:
# neuroglancer has positive y going down
# so apply these azimuth and roll corrections
# to fix orientatins
camera.Azimuth(-180)
camera.Roll(180)
# shift the camera posiiton and focal position
# to be centered on the desired location
p = camera.GetPosition()
p_new = np.array(p)+pos_nm
camera.SetPosition(*p_new)
camera.SetFocalPoint(*pos_nm)
return camera
[docs]def camera_from_ngl_state(state_d, zoom_factor=300.0):
"""define a vtk camera from a neuroglancer state dictionary
Parameters
----------
state_d: dict
an neuroglancer state dictionary
zoom_factor: float
how much to multiply zoom by to get camera backoff distance
default = 300 > ngl_zoom = 1 > 300 nm backoff distance
Returns
-------
vtk.vtkCamera
a vtk camera setup that mathces this state
"""
orient = state_d.get('perspectiveOrientation', [0.0, 0.0, 0.0, 1.0])
zoom = state_d.get('perspectiveZoom', 10.0)
position = state_d['navigation']['pose']['position']
pos_nm = np.array(position['voxelCoordinates'])*position['voxelSize']
camera = camera_from_quat(pos_nm, orient, zoom *
zoom_factor, ngl_correct=True)
return camera
[docs]def process_colors(color, xyz):
""" utility function to normalize colors on an set of things
Parameters
----------
color : np.array
a Nx3, or a N long, or a 3 long iterator the represents the
color or colors you want to label xyz with
xyz: np.array
a NxD matrix you wish to 'color'
Returns
-------
np.array
a Nx3 or N long array of color values
bool
map_colors, whether the colors should be mapped through a colormap
or used as is
"""
map_colors = False
if not isinstance(color, np.ndarray):
color = np.array(color)
if ((color.shape == (len(xyz), 3)) | (color.shape == (len(xyz), 4))):
# then we have explicit colors
if color.dtype != np.uint8:
# if not passing uint8 assume 0-1 mapping
assert(np.max(color) <= 1.0)
assert(np.min(color) >= 0)
color = np.uint8(color*255)
elif color.shape == (len(xyz),):
# then we want to map colors
map_colors = True
elif color.shape == (3,):
# then we have one explicit color
assert(np.max(color)<=1.0)
assert(np.min(color)>=0)
car = np.array(color*255, dtype=np.uint8)
color = np.repeat(car[np.newaxis,:],len(xyz),axis=0)
else:
raise ValueError(
'color must have shapse Nx3 if explicitly setting, or (N,) if mapping, or (3,)')
return color, map_colors
[docs]def mesh_actor(mesh,
color=(0, 1, 0),
opacity=0.1,
vertex_colors=None,
face_colors=None,
lut=None,
calc_normals=True,
show_link_edges=False,
line_width=3):
""" function for producing a vtkActor from a trimesh_io.Mesh
Parameters
----------
mesh : trimesh_io.Mesh
a mesh to visualize
color: various
a len 3 iterator of a solid color to label mesh
overridden by vertex_colors if passed
opacity: float
the opacity of the mesh (default .1)
vertex_colors: np.array
a np.array Nx3 list of explicit colors (where N is len(mesh.vertices))
OR
a np.array of len(N) list of values to map through a colormap
default (None) will use color to color mesh
face_colors: np.array
a np.array of Mx3 list of explicit colors (where M is the len(mesh.faces))
OR
a np.array of len(M) list of values to map through a colormap
(default None will use color for mesh)
lut: np.array
not implemented
calc_normals: bool
whether to calculate normals on the mesh. Default (True)
will take more time, but will render a smoother mesh
not compatible with sbow_link_edges. default True
show_link_edges: bool
whether to show the link_edges as lines. Will prevent calc_normals.
default False
line_width: int
how thick to show lines (default 3)
Returns
-------
vtk.vtkActor
vtkActor representing the mesh (to be passed to render_actors)
"""
if show_link_edges:
mesh_poly = trimesh_to_vtk(mesh.vertices, mesh.faces, mesh.link_edges)
else:
mesh_poly = trimesh_to_vtk(mesh.vertices, mesh.faces, None)
if vertex_colors is not None:
vertex_color, map_vertex_color = process_colors(
vertex_colors, mesh.vertices)
vtk_vert_colors = numpy_to_vtk(vertex_color)
vtk_vert_colors.SetName('colors')
mesh_poly.GetPointData().SetScalars(vtk_vert_colors)
if face_colors is not None:
face_color, map_face_colors = process_colors(face_colors, mesh.faces)
vtk_face_colors = numpy_to_vtk(face_color)
vtk_face_colors.SetName('colors')
mesh_poly.GetCellData().SetScalars(vtk_face_colors)
mesh_mapper = vtk.vtkPolyDataMapper()
if calc_normals and (not show_link_edges):
norms = vtk.vtkTriangleMeshPointNormals()
norms.SetInputData(mesh_poly)
mesh_mapper.SetInputConnection(norms.GetOutputPort())
else:
mesh_mapper.SetInputData(mesh_poly)
mesh_actor = vtk.vtkActor()
if lut is not None:
mesh_mapper.SetLookupTable(lut)
if face_colors is not None:
if map_face_colors:
mesh_mapper.SelectColorArray('colors')
mesh_mapper.ScalarVisibilityOn()
mesh_actor.SetMapper(mesh_mapper)
mesh_actor.GetProperty().SetLineWidth(line_width)
mesh_actor.GetProperty().SetColor(*color)
mesh_actor.GetProperty().SetOpacity(opacity)
return mesh_actor
[docs]def skeleton_actor(sk,
edge_property=None,
vertex_property=None,
vertex_data=None,
normalize_property=True,
color=(0, 0, 0),
line_width=3,
opacity=0.7,
lut_map=None):
"""
function to make a vtkActor from a skeleton class with different coloring options
Parameters
----------
sk : skeleton.Skeleton
the skeleton class to create a render
edge_property: str
the key to the edge_properties dictionary on the sk object to use for coloring
default None .. use color instead
vertex_property: str
the key to the vertex_properteis dictionary on the sk object to use for coloring
default NOne ... use color instead
vertex_data: np.array
what data to color skeleton vertices by
default None... use color intead
normalize_property: bool
whether to normalize the property data (edge/vertex) with dividing by np.nanmax
color: tuple
a 3 tuple in the [0,1] range of the color of the skeletoni
line_width: int
the width of the skeleton (default 3)
opacity: float
the opacity [0,1] of the mesh (1 = opaque, 0 = invisible)
lut_map: np.array
not implemented
Returns
-------
vtk.vtkActor
actor representing the skeleton
"""
sk_mesh = graph_to_vtk(sk.vertices, sk.edges)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(sk_mesh)
if edge_property is not None:
data = sk.edge_properties[edge_property]
if normalize_property:
data = data / np.nanmax(data)
sk_mesh.GetCellData().SetScalars(numpy_to_vtk(data))
lut = vtk.vtkLookupTable()
if lut_map is not None:
lut_map(lut)
lut.Build()
mapper.SetLookupTable(lut)
data = None
if vertex_data is None and vertex_property is not None:
data = sk.vertex_properties[vertex_property]
else:
data = vertex_data
if data is not None:
if normalize_property:
data = data / np.nanmax(data)
sk_mesh.GetPointData().SetScalars(numpy_to_vtk(data))
lut = vtk.vtkLookupTable()
if lut_map is not None:
lut_map(lut)
lut.Build()
mapper.ScalarVisibilityOn()
mapper.SetLookupTable(lut)
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetLineWidth(line_width)
actor.GetProperty().SetOpacity(opacity)
actor.GetProperty().SetColor(color)
return actor
[docs]def point_cloud_actor(xyz,
size=100,
color=(0,0,0),
opacity=1):
"""function to make a vtk.vtkActor from a set of xyz points that renders them as spheres
Parameters
----------
xyz : np.array
a Nx3 array of points
size: float or np.array
the size of each of the points, or a N long array of sizes of each point
color: len(3) iterator or np.array
the color of all the points, or the color of each point individually as a N long array
or a Nx3 list of explicit colors [0,1] range
opacity: float
the [0,1] opacity of mesh
Returns
-------
vtk.vtkActor
an actor with each of the xyz points as spheres of the specified size and color
"""
points = vtk.vtkPoints()
points.SetData(numpy_to_vtk(xyz, deep=True))
pc = vtk.vtkPolyData()
pc.SetPoints(points)
color, map_colors = process_colors(color, xyz)
vtk_colors = numpy_to_vtk(color)
vtk_colors.SetName('colors')
if np.isscalar(size):
size = np.full(len(xyz), size)
elif len(size) != len(xyz):
raise ValueError(
'Size must be either a scalar or an len(xyz) x 1 array')
pc.GetPointData().SetScalars(numpy_to_vtk(size))
pc.GetPointData().AddArray(vtk_colors)
ss = vtk.vtkSphereSource()
ss.SetRadius(1)
glyph = vtk.vtkGlyph3D()
glyph.SetInputData(pc)
glyph.SetInputArrayToProcess(3, 0, 0, 0, "colors")
glyph.SetColorModeToColorByScalar()
glyph.SetSourceConnection(ss.GetOutputPort())
glyph.SetScaleModeToScaleByScalar()
glyph.ScalingOn()
glyph.Update()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(glyph.GetOutputPort())
if map_colors:
mapper.SetScalarRange(np.min(color), np.max(color))
mapper.SelectColorArray('colors')
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetOpacity(opacity)
return actor
[docs]def linked_point_actor(vertices_a, vertices_b,
inds_a=None, inds_b=None,
line_width=1, color=(0, 0, 0), opacity=0.2):
""" function for making polydata with lines between pairs of points
Parameters
----------
vertices_a : np.array
a Nx3 array of point locations in xyz
vertices_b : np.array
a Nx3 array of point locations in xyz
inds_a: np.array
the indices in vertices_a to use (default None is all of them)
inds_b: np.array
the indices in vertices_b to use (default None is all of them)
line_width : int
the width of lines to draw (default 1)
color : iterator
a len(3) iterator (tuple, list, np.array) with the color [0,1] to use
opacity: float
a [0,1] opacity to render the lines
Returns
-------
vtk.vtkActor
an actor representing the lines between the points given with the color and opacity
specified. To be passed to render_actors
"""
if inds_a is None:
inds_a = np.arange(len(vertices_a))
if inds_b is None:
inds_b = np.arange(len(vertices_b))
if len(inds_a) != len(inds_b):
raise ValueError('Linked points must have the same length')
link_verts = np.vstack((vertices_a[inds_a], vertices_b[inds_b]))
link_edges = np.vstack((np.arange(len(inds_a)),
len(inds_a)+np.arange(len(inds_b))))
link_poly = graph_to_vtk(link_verts, link_edges.T)
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(link_poly)
link_actor = vtk.vtkActor()
link_actor.SetMapper(mapper)
link_actor.GetProperty().SetLineWidth(line_width)
link_actor.GetProperty().SetColor(color)
link_actor.GetProperty().SetOpacity(opacity)
return link_actor
[docs]def oriented_camera(center, up_vector=(0, -1, 0), backoff=500, backoff_vector=(0, 0, 1)):
'''
Generate a camera pointed at a specific location, oriented with a given up
direction, set to a backoff of the center a fixed distance with a particular direction
Parameters
----------
center : iterator
a len 3 iterator (tuple, list, np.array) with the x,y,z location of the camera's focus point
up_vector: iterator
a len 3 iterator (tuple, list, np.array) with the dx,dy,dz direction of the camera's up direction
default (0,-1,0) negative y is up.
backoff: float
distance in global space for the camera to be moved backward from the center point (default 500)
backoff_vector: iterator
a len 3 iterator (tuple, list, np.array) with the dx,dy,dz direction to back camera off of the focus point
Returns
-------
vtk.vtkCamera
the camera object representing the desired camera location, orientation and focus parameters
'''
camera = vtk.vtkCamera()
pt_center = center
vup = np.array(up_vector)
vup = vup/np.linalg.norm(vup)
bv = np.array(backoff_vector)
pt_backoff = pt_center - backoff * 1000 * bv
camera.SetFocalPoint(*pt_center)
camera.SetViewUp(*vup)
camera.SetPosition(*pt_backoff)
return camera
[docs]def render_actors_360(actors, directory, nframes, camera_start=None, start_frame=0,
video_width=1280, video_height=720, scale=4, do_save=True, back_color=(1, 1, 1)):
"""
Function to create a series of png frames which rotates around
the Azimuth angle of a starting camera
This will save images as a series of png images in the directory
specified.
The movie will start at time 0 and will go to frame nframes,
completing a 360 degree rotation in that many frames.
Keep in mind that typical movies are encoded at 15-30
frames per second and nframes is units of frames.
Parameters
----------
actors : list of vtkActor's
list of vtkActors to render
directory : str
folder to save images into
nframes : int
number of frames to render
camera_start : vtk.Camera
camera to start rotation, default=None will fit actors in scene
start_frame : int
number to save the first frame number as... (default 0)
i.e. frames will start_frame = 5, first file would be 005.png
video_width : int
size of video in pixels
video_height : int
size of the video in pixels
scale : int
how much to expand the image
do_save : bool
whether to save the images to disk or just play interactively
back_color : iterable
a len(3) iterable with the background color [0,1]rgb
Returns
-------
vtkRenderer
the renderer used to render
endframe
the last frame written
Example
-------
::
from meshparty import trimesh_io, trimesh_vtk
mm = trimesh_io.MeshMeta(disk_cache_path = 'meshes')
mesh = mm.mesh(filename='mymesh.obj')
mesh_actor = trimesh_vtk.mesh_actor(mesh)
mesh_center = np.mean(mesh.vertices, axis=0)
camera_start = trimesh_vtk.oriented_camera(mesh_center)
render_actors_360([mesh_actor], 'movie', 360, camera_start=camera_start)
"""
print('starting')
if camera_start is None:
frame_0_file = os.path.join(directory, "0000.png")
ren = render_actors(actors,
do_save=True,
filename=frame_0_file,
video_width=video_width,
video_height=video_height,
back_color=back_color)
print('done rendering')
camera_start = ren.GetActiveCamera()
print('camera_start done')
cameras = []
times = []
for k, angle in enumerate(np.linspace(0, 360, nframes)):
angle_cam = vtk.vtkCamera()
angle_cam.ShallowCopy(camera_start)
angle_cam.SetParallelProjection(camera_start.GetParallelProjection())
angle_cam.SetParallelScale(camera_start.GetParallelScale())
angle_cam.Azimuth(angle)
cameras.append(angle_cam)
times.append(k)
print('cameras ready')
return render_movie(actors, directory,
times=times,
cameras=cameras,
video_height=video_height,
video_width=video_width,
scale=scale,
do_save=do_save,
start_frame=start_frame,
back_color=back_color)
def _setup_renderer(video_width, video_height, back_color, camera=None):
ren = vtk.vtkRenderer()
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetSize(video_width, video_height)
ren.SetBackground(*back_color)
ren.UseFXAAOn()
# ren.SetBackground( 1, 1, 1)
if camera is not None:
ren.SetActiveCamera(camera)
# create a renderwindowinteractor
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
return ren, renWin, iren
[docs]def make_camera_interpolator(times, cameras, linear=False):
assert(len(times) == len(cameras))
camera_interp = vtk.vtkCameraInterpolator()
for t, cam in zip(times, cameras):
camera_interp.AddCamera(t, cam)
if linear:
camera_interp.SetInterpolationTypeToLinear()
return camera_interp
[docs]def render_movie(actors, directory, times, cameras, start_frame=0,
video_width=1280, video_height=720, scale=4,
do_save=True, back_color=(1, 1, 1), linear=False):
"""
Function to create a series of png frames based upon a defining
a set of cameras at a set of times.
This will save images as a series of png images in the directory
specified.
The movie will start at time 0 and will go to frame np.max(times)
Reccomend to make times start at 0 and the length of the movie
you want. Keep in mind that typical movies are encoded at 15-30
frames per second and times is units of frames.
Parameters
----------
actors : list of vtkActor's
list of vtkActors to render
directory : str
folder to save images into
times : np.array
array of K frame times to set the camera to
cameras : list of vtkCamera's
array of K vtkCamera objects. movie with have cameras[k]
at times[k].
start_frame : int
number to save the first frame number as... (default 0)
i.e. frames will start_frame = 5, first file would be 005.png
video_width : int
size of video in pixels
video_height : int
size of the video in pixels
scale : int
how much to expand the image
do_save : bool
whether to save the images to disk or just play interactively
Returns
-------
vtkRenderer
the renderer used to render
endframe
the last frame written
Example
-------
::
from meshparty import trimesh_io, trimesh_vtk
mm = trimesh_io.MeshMeta(disk_cache_path = 'meshes')
mesh = mm.mesh(filename='mymesh.obj')
mesh_actor = trimesh_vtk.mesh_actor(mesh)
mesh_center = np.mean(mesh.vertices, axis=0)
camera_start = trimesh_vtk.oriented_camera(mesh_center, backoff = 10000, backoff_vector=(0, 0, 1))
camera_180 = trimesh_vtk.oriented_camera(mesh_center, backoff = 10000, backoff_vector=(0, 0, -1))
times = np.array([0, 90, 180])
cameras = [camera_start, camera_180, camera_start]
render_movie([mesh_actor],
'movie',
times,
cameras)
"""
camera_interp=make_camera_interpolator(times, cameras, linear=linear)
def interpolate_camera(actors, camera, t):
camera_interp.InterpolateCamera(t, camera)
renWin, end_frame = render_movie_flexible(actors, directory, times,
interpolate_camera,
start_frame=start_frame,
video_width=video_width,
video_height=video_height,
scale=scale,
do_save=do_save,
back_color=back_color)
return renWin, end_frame
[docs]def render_movie_flexible(actors, directory, times, frame_change_function, start_frame=0,
video_width=1280, video_height=720, scale=4, camera=None,
do_save=True, back_color=(1, 1, 1)):
"""
Function to create a series of png frames based upon a defining
a frame change function that will alter actors and camera at
each time point
This will save images as a series of png images in the directory
specified.
The movie will start at time 0 and will go to frame np.max(times)
Reccomend to make times start at 0 and the length of the movie
you want. Keep in mind that typical movies are encoded at 15-30
frames per second and times is units of frames.
This is the most general of the movie making functions,
and can be used to custom change coloring of actors or their positions
over time using tranformations.
Parameters
----------
actors : list of vtkActor's
list of vtkActors to render
directory : str
folder to save images into
times : np.array
array of K frame times to set the camera to
frame_change_function : func
a function that takes (actors, camera, t) as arguments.
where actors are the list of actors passed here,
camera is the camera for the rendering,
and t is the current frame number.
This function may alter the actors and camera as a function
of time in some user defined manner.
start_frame : int
number to save the first frame number as... (default 0)
i.e. frames will start_frame = 5, first file would be 005.png
video_width : int
size of video in pixels
video_height : int
size of the video in pixels
scale : int
how much to expand the image
do_save : bool
whether to save the images to disk or just play interactively
Returns
-------
vtkRenderer
the renderer used to render
endframe
the last frame written
Example
-------
::
from meshparty import trimesh_io, trimesh_vtk
mm = trimesh_io.MeshMeta(disk_cache_path = 'meshes')
mesh = mm.mesh(filename='mymesh.obj')
mesh_actor = trimesh_vtk.mesh_actor(mesh)
mesh_center = np.mean(mesh.vertices, axis=0)
camera_start = trimesh_vtk.oriented_camera(mesh_center, backoff = 10000, backoff_vector=(0, 0, 1))
camera_180 = trimesh_vtk.oriented_camera(mesh_center, backoff = 10000, backoff_vector=(0, 0, -1))
times = np.array([0, 90, 180])
cameras = [camera_start, camera_180, camera_start]
render_movie([mesh_actor],
'movie',
times,
cameras)
"""
if not os.path.isdir(directory):
os.makedirs(directory)
if camera is None:
camera = vtk.vtkCamera()
# create a rendering window and renderer
ren, renWin, iren = _setup_renderer(
video_width, video_height, back_color, camera=camera)
for a in actors:
# assign actor to the renderer
ren.AddActor(a)
imageFilter = vtk.vtkWindowToImageFilter()
imageFilter.SetInput(renWin)
imageFilter.SetScale(scale)
imageFilter.SetInputBufferTypeToRGB()
imageFilter.ReadFrontBufferOff()
imageFilter.Update()
# Setup movie writer
if do_save:
moviewriter = vtk.vtkPNGWriter()
moviewriter.SetInputConnection(imageFilter.GetOutputPort())
renWin.OffScreenRenderingOn()
for i in np.arange(0, np.max(times)+1):
frame_change_function(actors, camera, i)
ren.ResetCameraClippingRange()
camera.ViewingRaysModified()
renWin.Render()
if do_save:
filename = os.path.join(directory, "%04d.png" % (i+start_frame))
moviewriter.SetFileName(filename)
# Export a current frame
imageFilter.Update()
imageFilter.Modified()
moviewriter.Write()
renWin.Finalize()
return renWin, i+start_frame
[docs]def scale_bar_actor(center, camera, length=10000, color=(0, 0, 0), linewidth=5, font_size=20):
"""Creates a xyz 3d scale bar actor located at a specific location with a given size
Parameters
----------
center : iterable
a length 3 iterable of xyz position
camera : vtk.vtkCamera
the camera the scale bar should follow
length : int, optional
length of each of the xyz axis, by default 10000
color : tuple, optional
color of text and lines, by default (0,0,0)
linewidth : int, optional
width of line in pixels, by default 5
font_size : int, optional
font size of xyz labels, by default 20
Returns
-------
vtk.vktActor
scale bar actor to add to render_actors
"""
axes_actor = vtk.vtkCubeAxesActor2D()
axes_actor.SetBounds(center[0], center[0]+length,
center[1], center[1]+length,
center[2], center[2]+length)
# this means no real labels
axes_actor.SetLabelFormat("")
axes_actor.SetCamera(camera)
# this turns off the tick marks and labelled numbers
axes_actor.SetNumberOfLabels(0)
# this affects whether the corner of the 3 axis
# changes as you rotate the view
# this option makes it stay constant
axes_actor.SetFlyModeToNone()
axes_actor.SetFontFactor(1.0)
axes_actor.GetProperty().SetColor(*color)
axes_actor.GetProperty().SetLineWidth(linewidth)
# this controls the color of text
tprop = vtk.vtkTextProperty()
tprop.SetColor(*color)
# no shadows on text
tprop.ShadowOff()
tprop.SetFontSize(font_size)
# makes the xyz and labels the same
axes_actor.SetAxisTitleTextProperty(tprop)
axes_actor.SetAxisLabelTextProperty(tprop)
return axes_actor
[docs]def values_to_colors(values, cmap, vmin=None, vmax=None):
"""
Function to map a set of values through a colormap
to get RGB values in order to facilitate coloring of meshes.
Parameters
----------
values: array-like, (n_vertices, )
values to pass through colormap
cmap: array-like, (n_colors, 3)
colormap describing the RGB values from vmin to vmax
vmin : float
(optional) value that should receive minimum of colormap.
default to minimum of values
vmax : float
(optional) values that should receive maximum of colormap
default to maximum of values
Output
------
colors: array-like, (n_vertices, 3)
RGB values for each entry in values (as np.uint8 [0-255])
Example
-------
Assuming mesh object and 'values' have been calculated already
::
import seaborn as sns
cmap = np.array(sns.color_palette('viridis', 1000))
clrs = trimesh_vtk.values_to_colors(values, cmap)
mesh_actor = trimesh_io.mesh_actor(mesh, vertex_colors = clrs, opacity=1.0)
trimesh_vtk.render_actors([mesh_actor])
"""
n_colors = cmap.shape[0]
if vmin is None:
vmin = np.nanmin(values)
if vmax is None:
vmax = np.nanmax(values)
values = np.clip(values, vmin, vmax)
r = np.interp(x=values, xp=np.linspace(vmin, vmax, n_colors), fp=cmap[:, 0])
g = np.interp(x=values, xp=np.linspace(vmin, vmax, n_colors), fp=cmap[:, 1])
b = np.interp(x=values, xp=np.linspace(vmin, vmax, n_colors), fp=cmap[:, 2])
colors = np.vstack([r, g, b]).T
colors = (colors * 255).astype(np.uint8)
return colors