Scalar filed is not volume rendered with a colormap
barikata1984 opened this issue · 1 comments
Hi, all
I am a graduate student using mayavi
for my research. I am trying to volome render
a scalar field
stored as a .csv
file. The shape of the field is rectangular, halved into two parts: the upper part with a scalar of 0.25 and the lower part with a scalar of 1.0. Since I am using the cool
colormap, I expected that the upper and lower parts should be rendered a translucent sky-blue volume and a translucent pink volume, respectively, like this example. However, as shown in the left figure attached below, the field is rendered mainly in translucent black, while the scalar field data itself is exactly what I expected (the upper and lower parts have a uniform scalar, respectively), as shown in the right plot with matplotlib
. Actually, the surfaces of the parts are painted somewhat sky-blue and pink, but the inside of the parts is black. The shape is exactly what I expected. Does someone come up with why my scalar field is volume rendered like that? And, can anybody give me any advice to resolve it?
Entries of the .csv file is an array of cartesian coordinates and a scalar at the location. The array's shape is (num_data_entries, (x, y, z, scalar)). The scalar is one of 0.0, 0.25, 1.0. The actual code to reproduce the rendering is as follows:
import numpy as np
from mayavi import mlab
from mayavi.core.lut_manager import LUTManager
from tvtk.util import ctf # ColorTransferFunction
def _get_custom_colormap(name, num_colors=256, opacity=1., transparent_input=-1.):
# Get a mayavi colormap as a numpy array
lm = LUTManager(number_of_colors=num_colors, lut_mode=name, show_scalar_bar=True)
rgbs = lm.lut.table.to_array()[:, :3] # ~ (:, RGBA) -> (:, RGB)
rgbs = rgbs.astype(float) / (num_colors - 1) # [0.0, 1.0]**3
# Setup a color transfer function with the above-generated colormap
_ctf = ctf.ColorTransferFunction()
for i, rgb in enumerate(rgbs):
_ctf.add_rgb_point(i/(num_colors - 1), *rgb)
# Setup a opacity transfer function
_otf = ctf.PiecewiseFunction()
_otf.add_point(transparent_input, 0.) # transparent
_otf.add_point(0., opacity)
_otf.add_point(1., opacity)
# Extend the input value range to allow transparent plot
_ctf.range = [transparent_input, 1.]
return _ctf, _otf
def _init_mayavi_volume_rendering_fig(x, y, z, scalars, _ctf=None, _otf=None):
# Initial rendering of mass distr
black = (0, 0, 0)
white = (1, 1, 1)
size = (720, 720)
figure = mlab.figure(bgcolor=white, fgcolor=black, size=size)
scalar_field = mlab.pipeline.scalar_field(x, y, z, scalars)
volume = mlab.pipeline.volume(scalar_field, figure=figure)
ctf_otf_update_necessary = False
if ctf is not None:
volume._ctf = _ctf
volume._volume_property.set_color(_ctf)
ctf_otf_update_necessary = True
if _otf is not None:
volume._otf = _otf
volume._volume_property.set_scalar_opacity(_otf)
ctf_otf_update_necessary = True
if ctf_otf_update_necessary:
# Apply the custom ctf
volume.update_ctf = True
return figure, volume
if __name__ == "__main__":
# generate gt mass distr ===========================================
aabb_scale = 0.64
density_1 = 500
density_2 = 2000
# voxel grid spec
level = 6
res = 2**level # resolution
half_res = res// 2
# scaled voxel spec
vx_side = aabb_scale / half_res
vx_volume = vx_side**3
# coordinates and mass distribution
ndc_coords = (np.arange(0, res) - half_res + .5) / half_res
coords = aabb_scale * ndc_coords
xyzs = np.array(np.meshgrid(coords, coords, coords, indexing="ij")) # ~ (3, res, res, res)
xyzs = xyzs.reshape(3, -1) # (3, N = res*res*res)
mass_distr = np.zeros_like(xyzs[0], dtype="float") # ~ (N, )
# upper part - - - - - - - - - - - - - - - - - - - - -
zidx = np.where(.0 <= xyzs[2])
xidx = np.where((-.25 * aabb_scale <= xyzs[0]) & (xyzs[0] <= .25 * aabb_scale))
yidx = np.where((-.5 * aabb_scale <= xyzs[1]) & (xyzs[1] <= .5 * aabb_scale))
xy_idx = np.intersect1d(xidx, yidx)
idx1 = np.intersect1d(xy_idx, zidx)
mass_distr[idx1] = density_1 * vx_volume
# lower part - - - - - - - - - - - - - - - - - - - - -
zidx = np.where(.0 >= xyzs[2])
idx2 = np.intersect1d(xy_idx, zidx)
mass_distr[idx2] = density_2 * vx_volume
print(f"{mass_distr.sum()=}")
# write the scalar field into a .csv ===============================
csv_filename = "gt_mass_distr.csv"
with open(csv_filename, 'w') as f:
writer = csv.writer(f)
for x, y, z, mass in zip(*xyzs, mass_distr):
writer.writerow([x, y, z, mass])
# load the .csv and plot w/ matplotlib =============================
loaded = np.loadtxt(csv_filename, delimiter=',') # ~ (N, (x, y, z, mass))
_x, _y, _z, _mass_distr = loaded.T
x = _x / aabb_scale
y = _y / aabb_scale
z = _z / aabb_scale
# set plot data
viridis = cm.get_cmap('viridis')
c = _mass_distr / _mass_distr.max()
s = np.zeros_like(_mass_distr)
s[_mass_distr.nonzero()] = 1
# plot
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')
size_const = 0.3
ax.scatter(x, y, z, s=size_const*s, c=c, cmap=viridis)
plt.show(block=False)
# volume render w/ mayavi ========================================
transparent_input = -1.
meshgrid_shape = (res, res, res)
scalars = transparent_input * np.ones(meshgrid_shape)
zero_to_one_md = _mass_distr / _mass_distr.max()
zero_to_one_md = zero_to_one_md.reshape(meshgrid_shape)
scalars[zero_to_one_md.nonzero()] = zero_to_one_md[zero_to_one_md.nonzero()]
print(f"{_mass_distr[_mass_distr.nonzero()].sum()=}")
print(f"{scalars[scalars.nonzero()].sum()*_mass_distr.max()=}")
_ctf, _otf = _get_custom_colormap("cool",
num_colors=256,
opacity=.7,
transparent_input=transparent_input)
figure, volume = _init_mayavi_volume_rendering_fig(x.reshape(meshgrid_shape),
y.reshape(meshgrid_shape),
z.reshape(meshgrid_shape),
scalars,
_ctf,
_otf)
max_epochs = 121
num_mayavi_camera_turn = 2
azimuth, _, _, _ = mlab.view()
azimuth_tick = num_mayavi_camera_turn * 360.0 / max_epochs
mlab.show()
Okay, the black area was assumed to be generated by the shading feature of vtk
. After I disabled it by adding the line volume._volume_property.shade = False
, the black part disappeared