Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/plotting/texture.py
T
2026-02-12 23:27:48 +08:00

697 lines
21 KiB
Python

"""Wrapper for :vtk:`vtkTexture`."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
import warnings
import numpy as np
import pyvista
from pyvista.core.dataobject import DataObject
from pyvista.core.utilities.fileio import _try_imageio_imread
from pyvista.core.utilities.misc import AnnotatedIntEnum
from . import _vtk
if TYPE_CHECKING:
from pyvista.core._typing_core import NumpyArray
class Texture(DataObject, _vtk.vtkTexture):
"""Wrap :vtk:`vtkTexture`.
Textures can be used to apply images to surfaces, as in the case of
:ref:`texture_example`.
They can also be used for environment textures to affect the lighting of
the scene, or even as a environment cubemap as in the case of
:ref:`pbr_example` and :ref:`planets_example`.
Parameters
----------
uinput : str, :vtk:`vtkImageData`, :vtk:`vtkTexture`, sequence[ImageData], optional
Filename, :vtk:`vtkImageData`, :vtk:`vtkTexture`, :class:`numpy.ndarray` or a
sequence of images to create a cubemap. If a sequence of images, must
be of the same size and in the following order:
* +X
* -X
* +Y
* -Y
* +Z
* -Z
**kwargs : dict, optional
Optional arguments when reading from a file. Generally unused.
Examples
--------
Load a texture from file. File should be a "image" or "image-like" file.
>>> from pathlib import Path
>>> import pyvista as pv
>>> from pyvista import examples
>>> path = examples.download_masonry_texture(load=False)
>>> Path(path).name
'masonry.bmp'
>>> texture = pv.Texture(path)
>>> texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 256, 256
Create a texture from an RGB array. Note how this is colored per "point"
rather than per "pixel".
>>> import numpy as np
>>> arr = np.array(
... [
... [255, 255, 255],
... [255, 0, 0],
... [0, 255, 0],
... [0, 0, 255],
... ],
... dtype=np.uint8,
... )
>>> arr = arr.reshape((2, 2, 3))
>>> texture = pv.Texture(arr)
>>> texture.plot()
Create a cubemap from 6 images.
>>> px = examples.download_sky(direction='posx') # doctest:+SKIP
>>> nx = examples.download_sky(direction='negx') # doctest:+SKIP
>>> py = examples.download_sky(direction='posy') # doctest:+SKIP
>>> ny = examples.download_sky(direction='negy') # doctest:+SKIP
>>> pz = examples.download_sky(direction='posz') # doctest:+SKIP
>>> nz = examples.download_sky(direction='negz') # doctest:+SKIP
>>> texture = pv.Texture([px, nx, py, ny, pz, nz]) # doctest:+SKIP
>>> texture.cube_map # doctest:+SKIP
True
"""
class WrapType(AnnotatedIntEnum):
"""Types of wrapping a texture can support.
Wrap mode for the texture coordinates valid values are:
* CLAMP_TO_EDGE
* REPEAT (Default in :class:`pyvista.Texture`)
* MIRRORED_REPEAT
* CLAMP_TO_BORDER
See :attr:`Texture.wrap` for usage.
"""
CLAMP_TO_EDGE = (0, 'Clamp to edge')
REPEAT = (1, 'Repeat')
MIRRORED_REPEAT = (2, 'Mirrored repeat')
CLAMP_TO_BORDER = (3, 'Clamp to border')
def __init__(self, uinput=None, **kwargs):
"""Initialize the texture."""
super().__init__(uinput)
if isinstance(uinput, _vtk.vtkTexture):
self._from_texture(uinput)
elif isinstance(uinput, np.ndarray):
self._from_array(uinput)
elif isinstance(uinput, _vtk.vtkImageData):
self._from_image_data(uinput)
elif isinstance(uinput, str):
self._from_file(filename=uinput, **kwargs)
elif isinstance(uinput, Sequence) and len(uinput) == 6:
# Create a cubemap
self.mipmap = True
self.interpolate = True
self.cube_map = True # Must be set prior to setting images
# add each image to the cubemap
for i, image in enumerate(uinput):
if not isinstance(image, pyvista.ImageData):
msg = (
'If a sequence, the each item in the first argument must be a '
'pyvista.ImageData'
)
raise TypeError(msg)
# must flip y for cubemap to display properly
self.SetInputDataObject(i, image._flip_uniform(1))
elif uinput is None:
pass
else:
msg = f'Cannot create a pyvista.Texture from ({type(uinput)})'
raise TypeError(msg)
def _from_file(self, filename, **kwargs):
try:
image = pyvista.read(filename, **kwargs)
if image.n_points < 2: # pragma: no cover
msg = 'Problem reading the image with VTK.'
raise RuntimeError(msg)
self._from_image_data(image)
except (KeyError, ValueError, OSError):
self._from_array(_try_imageio_imread(filename)) # pragma: no cover
def _from_texture(self, texture):
image = texture.GetInput()
self._from_image_data(image)
@property
def interpolate(self) -> bool: # numpydoc ignore=RT01
"""Return if interpolate is enabled or disabled.
Examples
--------
Show the masonry texture without interpolation. Here, we zoom to show
the individual pixels.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.interpolate = False
>>> texture.plot(cpos='xy', zoom=3)
Plot the same texture with interpolation.
>>> texture.interpolate = True
>>> texture.plot(cpos='xy', zoom=3)
"""
return bool(self.GetInterpolate())
@interpolate.setter
def interpolate(self, value: bool):
self.SetInterpolate(value)
@property
def mipmap(self) -> bool: # numpydoc ignore=RT01
"""Return if mipmap is enabled or disabled."""
return bool(self.GetMipmap())
@mipmap.setter
def mipmap(self, value: bool):
self.SetMipmap(value)
def _from_image_data(self, image):
if not isinstance(image, pyvista.ImageData):
image = pyvista.ImageData(image)
self.SetInputDataObject(image)
self.Update()
def _from_array(self, image):
"""Create a texture from a np.ndarray."""
if image.ndim not in [2, 3]:
# we support 2 [single component image] or 3 [e.g. rgb or rgba] dims
msg = 'Input image must be nn by nm by RGB[A]'
raise ValueError(msg)
if image.ndim == 3:
if image.shape[2] not in [1, 3, 4]:
msg = 'Third dimension of the array must be of size 3 (RGB) or 4 (RGBA)'
raise ValueError(msg)
n_components = image.shape[2]
elif image.ndim == 2:
n_components = 1
grid = pyvista.ImageData(dimensions=(image.shape[1], image.shape[0], 1))
grid.point_data['Image'] = np.flip(image.swapaxes(0, 1), axis=1).reshape(
(-1, n_components),
order='F',
)
grid.set_active_scalars('Image')
self._from_image_data(grid)
@property
def repeat(self) -> bool: # numpydoc ignore=RT01
"""Repeat the texture.
This is provided for convenience and backwards compatibility.
For new code, use :func:`Texture.wrap`.
Examples
--------
Load the masonry texture and create a simple :class:`pyvista.PolyData`
with texture coordinates using :func:`pyvista.Plane`. By default the
texture coordinates are between 0 and 1. Let's raise these values over
1 by multiplying them in place. This will allow us to wrap the texture.
>>> import pyvista as pv
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates *= 2
This is the texture plotted with repeat set to ``False``.
>>> texture.repeat = False
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
This is the texture plotted with repeat set to ``True``.
>>> texture.repeat = True
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
return bool(self.GetRepeat())
@repeat.setter
def repeat(self, flag: bool):
self.SetRepeat(flag)
def flip_x(self) -> Texture:
"""Flip the texture in the x direction.
Returns
-------
pyvista.Texture
Flipped texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> flipped = texture.flip_x()
>>> flipped.plot()
"""
return Texture(self.to_image()._flip_uniform(0)) # type: ignore[abstract]
def flip_y(self) -> Texture:
"""Flip the texture in the y direction.
Returns
-------
pyvista.Texture
Flipped texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> flipped = texture.flip_y()
>>> flipped.plot()
"""
return Texture(self.to_image()._flip_uniform(1)) # type: ignore[abstract]
def to_image(self):
"""Return the texture as an image.
Returns
-------
pyvista.ImageData
Texture represented as a uniform grid.
"""
return self.GetInput()
def to_array(self) -> NumpyArray[float]:
"""Return the texture as an array.
Notes
-----
The shape of the array's first two dimensions will be swapped. For
example, a ``(300, 200)`` image will return an array of ``(200, 300)``.
Returns
-------
numpy.ndarray
Texture as a numpy array.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 1600, 1200
>>> texture.to_array().shape
(1200, 1600, 3)
>>> texture.to_array().dtype
dtype('uint8')
"""
return self.to_image().active_scalars.reshape(
[*list(self.dimensions)[::-1], self.n_components]
)[::-1]
def rotate_cw(self) -> Texture:
"""Rotate this texture 90 degrees clockwise.
Returns
-------
pyvista.Texture
Rotated texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> rotated = texture.rotate_cw()
>>> rotated.plot()
"""
return Texture(np.rot90(self.to_array())) # type: ignore[abstract]
def rotate_ccw(self) -> Texture:
"""Rotate this texture 90 degrees counter-clockwise.
Returns
-------
pyvista.Texture
Rotated texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_puppy_texture()
>>> rotated = texture.rotate_ccw()
>>> rotated.plot()
"""
return Texture(np.rot90(self.to_array(), k=3)) # type: ignore[abstract]
@property
def cube_map(self) -> bool: # numpydoc ignore=RT01
"""Return ``True`` if cube mapping is enabled and ``False`` otherwise."""
return self.GetCubeMap()
@cube_map.setter
def cube_map(self, flag: bool):
self.SetCubeMap(flag)
def copy(self): # type: ignore[override]
"""Make a copy of this texture.
Returns
-------
pyvista.Texture
Copied texture.
"""
return Texture(self.to_image().copy()) # type: ignore[abstract]
def to_skybox(self):
"""Return the texture as a :vtk:`vtkSkybox` if cube mapping is enabled.
Returns
-------
:vtk:`vtkSkybox`
Skybox if cube mapping is enabled. Otherwise, ``None``.
"""
if self.cube_map:
skybox = _vtk.vtkSkybox()
skybox.SetTexture(self)
return skybox
return None
def __repr__(self):
"""Return the object representation."""
return pyvista.DataSet.__repr__(self) # type: ignore[type-var]
def _get_attrs(self):
"""Return the representation methods (internal helper)."""
attrs = []
attrs.append(('Components', self.n_components, '{:d}'))
attrs.append(('Cube Map', self.cube_map, '{:}'))
attrs.append(('Dimensions', self.dimensions, '{:d}, {:d}')) # type: ignore[arg-type]
return attrs
@property
def n_components(self) -> int: # numpydoc ignore=RT01
"""Return the number of components in the image.
In textures, 3 or 4 components are used for representing RGB and RGBA
images.
Examples
--------
Show the number of components in the example masonry texture.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.n_components
3
"""
input_data = self.GetInput()
if input_data is None:
return 0
return input_data.GetPointData().GetScalars().GetNumberOfComponents()
@property
def dimensions(self) -> tuple[int, int]: # numpydoc ignore=RT01
"""Dimensions of the texture.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.dimensions
(256, 256)
"""
input_data = self.GetInput()
if input_data is None:
return (0, 0)
return input_data.GetDimensions()[:2]
def plot(self, **kwargs):
"""Plot the texture as an image.
If the texture is a cubemap, it will be displayed as a skybox with a
sphere in the center reflecting the environment.
Parameters
----------
**kwargs : dict, optional
Optional keyworld arguments. See :func:`pyvista.plot`.
Returns
-------
pyvista.Actor | None
See the returns section of :func:`pyvista.plot`.
Examples
--------
Plot a simple texture.
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> texture.plot()
Plot a cubemap as a skybox.
>>> cube_map = examples.download_sky_box_cube_map()
>>> cube_map.plot()
"""
if self.cube_map:
return self._plot_skybox(**kwargs)
kwargs.setdefault('zoom', 'tight')
kwargs.setdefault('lighting', False)
kwargs.setdefault('show_axes', False)
kwargs.setdefault('show_scalar_bar', False)
mesh = pyvista.Plane(i_size=self.dimensions[0], j_size=self.dimensions[1])
return mesh.plot(texture=self, **kwargs)
def _plot_skybox(self, **kwargs):
"""Plot this texture as a skybox."""
cpos = kwargs.pop('cpos', 'xy')
zoom = kwargs.pop('zoom', 0.5)
show_axes = kwargs.pop('show_axes', True)
lighting = kwargs.pop('lighting', None)
pl = pyvista.Plotter(lighting=lighting)
pl.add_actor(self.to_skybox())
pl.set_environment_texture(self, is_srgb=True)
pl.add_mesh(pyvista.Sphere(), pbr=True, roughness=0.5, metallic=1.0)
pl.camera_position = cpos
pl.camera.zoom(zoom)
if show_axes:
pl.show_axes()
pl.show(**kwargs)
@property
def wrap(self) -> Texture.WrapType: # numpydoc ignore=RT01
"""Return or set the Wrap mode for the texture coordinates.
Wrap mode for the texture coordinates valid values are:
* ``0`` - CLAMP_TO_EDGE
* ``1`` - REPEAT
* ``2`` - MIRRORED_REPEAT
* ``3`` - CLAMP_TO_BORDER
Notes
-----
CLAMP_TO_BORDER is not supported with OpenGL ES <= 3.2. Wrap will
default to CLAMP_TO_EDGE if it is set to CLAMP_TO_BORDER in this case.
Requires ``vtk`` v9.1.0 or newer.
Examples
--------
Load the masonry texture and create a simple :class:`pyvista.PolyData`
with texture coordinates using :func:`pyvista.Plane`. By default the
texture coordinates are between 0 and 1. Let's raise these values over
1 by multiplying them in place. This will allow us to wrap the texture.
>>> import pyvista as pv
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates *= 2
Let's now set the texture wrap to clamp to edge and visualize it.
>>> texture.wrap = pv.Texture.WrapType.CLAMP_TO_EDGE
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
Here is the default repeat:
>>> texture.wrap = pv.Texture.WrapType.REPEAT
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
And here is mirrored repeat:
>>> texture.wrap = pv.Texture.WrapType.MIRRORED_REPEAT
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
Finally, this is clamp to border:
>>> texture.wrap = pv.Texture.WrapType.CLAMP_TO_BORDER
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane, texture=texture)
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
if not hasattr(self, 'GetWrap'): # pragma: no cover
from pyvista.core.errors import VTKVersionError # noqa: PLC0415
msg = '`wrap` requires VTK v9.1.0 or newer.'
raise VTKVersionError(msg)
return Texture.WrapType(self.GetWrap()) # type: ignore[call-arg]
@wrap.setter
def wrap(self, value: Texture.WrapType | int):
if not hasattr(self, 'SetWrap'): # pragma: no cover
from pyvista.core.errors import VTKVersionError # noqa: PLC0415
msg = '`wrap` requires VTK v9.1.0 or newer.'
raise VTKVersionError(msg)
self.SetWrap(value)
def to_grayscale(self) -> Texture:
"""Convert this texture as a single component (grayscale) texture.
Returns
-------
pyvista.Texture
Texture converted to grayscale. If already grayscale, the original
texture itself is returned.
Notes
-----
The transparency channel (if available) will be dropped.
Follows the `CCIR 601 <https://en.wikipedia.org/wiki/Rec._601>`_ luma
calculation equation of ``Y = 0.299*R + 0.587*G + 0.114*B``.
Examples
--------
>>> from pyvista import examples
>>> texture = examples.download_masonry_texture()
>>> bw_texture = texture.to_grayscale()
>>> bw_texture
Texture (...)
Components: 1
Cube Map: False
Dimensions: 256, 256
>>> bw_texture.plot()
"""
if self.n_components == 1:
return self.copy()
data = self.to_array()
r, g, b = data[..., 0], data[..., 1], data[..., 2]
data = (0.299 * r + 0.587 * g + 0.114 * b).round().astype(np.uint8)
return Texture(data) # type: ignore[abstract]
def image_to_texture(image):
"""Convert :class:`pyvista.ImageData` to a :class:`pyvista.Texture`.
Parameters
----------
image : pyvista.ImageData | :vtk:`vtkImageData`
Image to convert.
Returns
-------
pyvista.Texture
The texture.
"""
return Texture(image) # type: ignore[abstract]
def numpy_to_texture(image):
"""Convert a NumPy image array to a :class:`pyvista.Texture`.
Parameters
----------
image : numpy.ndarray
Numpy image array. Texture datatype expected to be ``np.uint8``.
Returns
-------
pyvista.Texture
PyVista texture.
Examples
--------
Create an all white texture.
>>> import pyvista as pv
>>> import numpy as np
>>> tex_arr = np.ones((1024, 1024, 3), dtype=np.uint8) * 255
>>> tex = pv.numpy_to_texture(tex_arr)
"""
if image.dtype != np.uint8:
image = image.astype(np.uint8)
warnings.warn(
'Expected `image` dtype to be ``np.uint8``. `image` has been copied '
'and converted to np.uint8.',
UserWarning,
)
return Texture(image) # type: ignore[abstract]