pyvista / pyvista-support

[moved] Please head over to the Discussions tab of the PyVista repository
https://github.com/pyvista/pyvista/discussions
59 stars 4 forks source link

Opacity based on cell-volume data #482

Open TBody opened 2 years ago

TBody commented 2 years ago

Setting opacity based on cell-volume data

PyVista newbie, sorry! I'm trying to visualize some 3D data on an unstructured grid (a collection of connected 2D hexahedra, non-overlapping). The dataset is somewhat peculiar in that it has high resolution in 2 dimensions, but low resolution in the other dimension, and so the hexahedra have a small width and breadth, but a very large length (~1000x the other dimensions).

I'd like to set the opacity of the cell based on its value, and so I've tried

mesh = pv.read('simple.vtk')
mesh.plot(opacity='linear')

I find a similar behavior in Paraview with "Opacity for surfaces", so I believe that this is me not understanding VTK rather than an issue in PyVista. However, I'm not sure how to go about how to get the desired behavior, any pointers greatly appreciated!

image

Example Data

simple.vtk.zip

adamgranthendry commented 2 years ago

@TBody No worries! This seems to be a common question. I'll reiterate my answer from volumetric rendering for volumetric data #472 .

TLDR: To set the opacity for everything to a constant values, you need to make opacity an array of 256 floats with the same value. For example, to make the opacity half (on a scale of 0 to 1 255, of course):

opacity = np.ones((256,)) * 0.5

In regards to your second question, pyvista.add_volume uses 256 colors on the scale bar by default (controlled by the n_colors argument). Your data has 3 color channels and an alpha channel (R, G, B, and A), but assuming each color channel is 8-bit, this means you can represent (2**8)**3 = 2**24 colors (a big gamut!). The n_colors is made 256 (i.e. 2**8) so the lookup table (LUT) for opacity can be contained in one byte and thus provide faster lookups. The opacity argument is intended to be the same length as n_colors, but you can specify fewer arguments.

In essence, when you specify [0, 1, 0] for opacity, you are setting the opacity for the first 3 colors (out of the total 256 colors) to 0, 1, and 0, respectively. The remaining 253 colors preserve their default values. When you specify [0, 1], you set only the first two colors, and the remaining 254 retain their value.

I believe from your code that you think you are setting the opacity of the color channels, but that is not actually how opacity works in pyvista.add_volume. You can instead create an opacity array of length 256 that has varying degrees of opacity from 0 to 1. For example, opacity = np.linspace(0, 1, 256) would make a linearly increasing opacity from 0 at the low values of your data to 1 at the high values. Instead, pyvista already comes with a bunch of pre-made opacity functions (called opacity transfer functions in VTK and pyvista), so you can specify a string that maps to a pre-made opacity transfer function.

The opacity transfer functions are specified in pyvista.plotting.tools in the function opacity_transfer_function. You have the following to choose from:

 transfer_func = {
        'linear': np.linspace(0, 255, n_colors, dtype=np.uint8),
        'geom': np.geomspace(1e-6, 255, n_colors, dtype=np.uint8),
        'geom_r': np.geomspace(255, 1e-6, n_colors, dtype=np.uint8),
        'sigmoid': sigmoid(np.linspace(-10.,10., n_colors)),
        'sigmoid_3': sigmoid(np.linspace(-3.,3., n_colors)),
        'sigmoid_4': sigmoid(np.linspace(-4.,4., n_colors)),
        'sigmoid_5': sigmoid(np.linspace(-5.,5., n_colors)),
        'sigmoid_6': sigmoid(np.linspace(-6.,6., n_colors)),
        'sigmoid_7': sigmoid(np.linspace(-7.,7., n_colors)),
        'sigmoid_8': sigmoid(np.linspace(-8.,8., n_colors)),
        'sigmoid_9': sigmoid(np.linspace(-9.,9., n_colors)),
        'sigmoid_10': sigmoid(np.linspace(-10.,10., n_colors)),
    }

For example, sigmoid will give you a nice continuous variation in opacity. In addition, if you would prefer to reverse the color bar so that low values have more opacity, you can add _r to any of the strings above and it will reverse the colors for you.

I would recommend using sigmoid as that seems to work nice most of the time. In addition, if you want to "filter out" some values from your data set, I would use clim because I think None values won't work, though I could be wrong. For example, you could try clim=[np.nanmin(scalars_a[scalars_a>0]), np.nanmax(scalars_a)].

@MatthewFlamm @akaszynski This would be another great thing to add to our documentation! The volume rendering section could use some TLC.

adamgranthendry commented 2 years ago

@TBody If this answers your question, please kindly let me know so we can close the issue. No rush!

akaszynski commented 2 years ago

@adamgranthendry, you're correct, we need to add that. Right now I've got three PRs in the oven. Would you mind submitting one for this? It's a great example.

adamgranthendry commented 2 years ago

@akaszynski You bet! I know I still owe you some more feedback on one of the other ones too.

TBody commented 2 years ago

@adamgranthendry Thanks! Reading more into it, I think what I was actually trying to do was volume rendering: i.e. set the cell volumes as semi-transparent, rather than cell-surface rendering.

If I set, for instance opacity=np.ones((256,))*255/2 I still don't change the volume opacity (see attached).

image

If I try volume=True then I get the error "Type <class 'pyvista.core.pointset.UnstructuredGrid'> not supported for volume rendering at this time." -- so I think this is an issue regarding my choice of mesh.

P.S. I found that the opacity range has to be [0, 255] rather than [0, 1] (using pyvista 0.31.3 pyhd8ed1ab_0 conda-forge)

adamgranthendry commented 2 years ago

@TBody Oh yes, you're right, my mistake. The docstring clearly states opacity is on the scale of [0, 255]. And yes, pyvista currently only supports UniformGrid data (i.e. ImageData in VTK), although VTK has vtkUnstructuredGridVolumeRayCastMapper.

From what I can see though, your opacity has changed. It has increased for your lower values and decreased for your higher ones. Previously, you were using linear, so your color bar scale was going from 0% opacity ("translucent") to 100% opacity ("opaque"). Now, with opacity set at 127.5, the intensity is 50% all throughout:

Linear: image

50% Opaque: image

Do you see how in the first instance, the left end of the color bar is clear and the right end is very intense, but how in the second instance the color is flat all throughout?

Maybe we can figure out how to correctly set the opacity for you so you can see what you're trying to see. What is it exactly that you would like to do?