From ce7c558cf502702c902b45e54d23abc0e40f4c87 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Sat, 11 May 2024 09:39:20 -0600 Subject: [PATCH 01/19] Add point and cell voxel conversion filters --- pyvista/core/filters/image_data.py | 120 +++++++++++++++++++++++++++ tests/core/test_imagedata_filters.py | 45 ++++++++++ 2 files changed, 165 insertions(+) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 61385c208e..64fdd4487e 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -3,6 +3,7 @@ from __future__ import annotations import collections.abc +import operator from typing import TYPE_CHECKING, Literal, Optional, Union, cast import numpy as np @@ -950,6 +951,125 @@ def contour_labeled( _vtk.vtkLogger.SetStderrVerbosity(verbosity) return cast(pyvista.PolyData, wrap(alg.GetOutput())) + def point_voxels_to_cell_voxels(self): + """Convert point voxel data to cell voxel data. + + Convert voxel data represented as points in a uniform grid into voxel cells + in a uniform grid. The conversion is performed such that the input points have + the same world coordinates as the centers of the converted voxel cells. + + Since many filters are inherently either point filters (e.g. ImageDataFilters) + or cell filters (e.g. DataSetFilters), this conversion enables point voxel data + to be used with cell-based filters while ensuring the voxels have the + appropriate representation. + + All point data (if any) is converted into cell data. Any active point scalars + will remain active as cell scalars in the output. If the input contains cell + data, it is ignored and removed the output. The dimensions of the returned + image are all increased by one relative to the input dimensions. + + Returns + ------- + pyvista.ImageData + Image with voxels represented as cells. + """ + return self._convert_voxels(points_to_cells=True) + + def cell_voxels_to_point_voxels(self): + """Convert cell voxel data to point voxel data. + + Convert voxel data represented as voxel cells in a uniform grid into points + in a uniform grid. The conversion is performed such that the centers of the + input voxel cells have the same world coordinates as the converted points. + + Since many filters are inherently either point filters (e.g. ImageDataFilters) + or cell filters (e.g. DataSetFilters), this conversion enables point voxel data + to be used with cell-based filters while ensuring the voxels have the + appropriate representation. + + All cell data (if any) is converted into point data. Any active cell scalars + will remain active as point scalars in the output. If the input contains point + data, it is ignored and removed the output. The dimensions of the returned + image are all decreased by one relative to the input dimensions. + + Returns + ------- + pyvista.ImageData + Image with voxels represented as points. + """ + return self._convert_voxels(points_to_cells=False) + + def _convert_voxels(self, points_to_cells: bool): + """Convert point voxels to cell voxels or vice-versa. + + If there are active scalars for the input voxels, they will be set to active + for the output voxels. For example, if converting point voxels to cell voxels, + and the input has active point scalars, the same scalar name will be made active + for returned cell voxels (as active cell scalars). + + Parameters + ---------- + points_to_cells : bool + Set to ``True`` to convert point voxels to cell voxels. + Set to ``False`` to convert cell voxels to point voxels. + + Returns + ------- + pyvista.ImageData + Image with converted voxels. + """ + + def _get_output_scalars(preference): + active_scalars = self.active_scalars_name + if active_scalars: + field = self.get_array_association( + active_scalars, + preference=preference, + ) + active_scalars = active_scalars if field.name.lower() == preference else None + return active_scalars + + point_data = self.point_data # type: ignore[attr-defined] + cell_data = self.cell_data # type: ignore[attr-defined] + + # Get data to use and operations to perform for the conversion + new_image = pyvista.ImageData() + if points_to_cells: + output_scalars = _get_output_scalars('point') + # Enlarge image so points become cell centers + origin_operator = operator.sub + dims_operator = operator.add # Increase dimensions + old_data = point_data + new_data = new_image.cell_data + else: # cells_to_points + output_scalars = _get_output_scalars('cell') + # Shrink image so cell centers become points + origin_operator = operator.add + dims_operator = operator.sub # Decrease dimensions + old_data = cell_data + new_data = new_image.point_data + + new_image.origin = origin_operator( + self.origin, # type: ignore[attr-defined] + np.array(self.spacing) / 2, # type: ignore[attr-defined] + ) + new_image.dimensions = dims_operator( + np.array(self.dimensions), # type: ignore[attr-defined] + 1, + ) + new_image.spacing = self.spacing # type: ignore[attr-defined] + new_image.SetDirectionMatrix(self.GetDirectionMatrix()) # type: ignore[attr-defined] + + # Copy field data + new_image.field_data.update(self.field_data) # type: ignore[attr-defined] + + # Copy old data (point or cell) to new data (cell or point) + for array_name in old_data.keys(): + new_data[array_name] = old_data[array_name] + + new_image.set_active_scalars(output_scalars) + return new_image + def pad_image( self, pad_value: Union[float, VectorLike[float], Literal['wrap', 'mirror']] = 0.0, diff --git a/tests/core/test_imagedata_filters.py b/tests/core/test_imagedata_filters.py index f42e2e57c8..6b2bb09a81 100644 --- a/tests/core/test_imagedata_filters.py +++ b/tests/core/test_imagedata_filters.py @@ -129,6 +129,51 @@ def test_contour_labeled_with_invalid_scalars(): label_map.contour_labeled() +@pytest.fixture() +def uniform_many_scalars(uniform): + uniform['Spatial Point Data2'] = uniform['Spatial Point Data'] * 2 + uniform['Spatial Cell Data2'] = uniform['Spatial Cell Data'] * 2 + return uniform + + +@pytest.mark.parametrize( + 'active_scalars', + [None, 'Spatial Point Data2', 'Spatial Point Data'], +) +def test_point_voxels_to_cell_voxels(uniform_many_scalars, active_scalars): + uniform_many_scalars.set_active_scalars(active_scalars) + + point_voxel_image = uniform_many_scalars + point_voxel_points = point_voxel_image.points + + cell_voxel_image = point_voxel_image.point_voxels_to_cell_voxels() + cell_voxel_center_points = cell_voxel_image.cell_centers().points + + assert cell_voxel_image.active_scalars_name == active_scalars + assert set(cell_voxel_image.array_names) == {'Spatial Point Data', 'Spatial Point Data2'} + assert np.array_equal(point_voxel_points, cell_voxel_center_points) + assert np.array_equal(point_voxel_image.active_scalars, cell_voxel_image.active_scalars) + + +@pytest.mark.parametrize( + 'active_scalars', + [None, 'Spatial Cell Data2', 'Spatial Cell Data'], +) +def test_cell_voxels_to_point_voxels(uniform_many_scalars, active_scalars): + uniform_many_scalars.set_active_scalars(active_scalars) + + cell_voxel_image = uniform_many_scalars + cell_voxel_center_points = cell_voxel_image.cell_centers().points + + point_voxel_image = cell_voxel_image.cell_voxels_to_point_voxels() + point_voxel_points = point_voxel_image.points + + assert cell_voxel_image.active_scalars_name == active_scalars + assert set(point_voxel_image.array_names) == {'Spatial Cell Data', 'Spatial Cell Data2'} + assert np.array_equal(cell_voxel_center_points, point_voxel_points) + assert np.array_equal(cell_voxel_image.active_scalars, point_voxel_image.active_scalars) + + @pytest.fixture() def single_point_image(): image = pv.ImageData(dimensions=(1, 1, 1)) From 9d69fa2df46f083a16abf80ee4e6ee6308faf0f9 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Sat, 11 May 2024 09:40:41 -0600 Subject: [PATCH 02/19] Add user_dict tests for field data --- tests/core/test_dataobject.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/core/test_dataobject.py b/tests/core/test_dataobject.py index f6e80e0079..15a97879dc 100644 --- a/tests/core/test_dataobject.py +++ b/tests/core/test_dataobject.py @@ -196,3 +196,15 @@ def test_user_dict_persists_with_pack_labels_filter(): image.user_dict['name'] = 'image' image = image.pack_labels() assert image.user_dict['name'] == 'image' + + +def test_user_dict_persists_with_point_voxels_to_cell_voxels(uniform): + uniform.user_dict['name'] = 'image' + uniform.cell_voxels_to_point_voxels() + assert uniform.user_dict['name'] == 'image' + + +def test_user_dict_persists_with_cell_voxels_to_point_voxels(uniform): + uniform.user_dict['name'] = 'image' + uniform.point_voxels_to_cell_voxels() + assert uniform.user_dict['name'] == 'image' From 03e50dedf8c59cd1378c882fceb4d838757f0805 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Sat, 11 May 2024 11:28:33 -0600 Subject: [PATCH 03/19] Add examples --- pyvista/core/filters/image_data.py | 112 ++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 64fdd4487e..59d7bac952 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -965,13 +965,68 @@ def point_voxels_to_cell_voxels(self): All point data (if any) is converted into cell data. Any active point scalars will remain active as cell scalars in the output. If the input contains cell - data, it is ignored and removed the output. The dimensions of the returned + data, it is ignored and removed from the output. The dimensions of the returned image are all increased by one relative to the input dimensions. + .. versionadded:: 0.44.0 + + See Also + -------- + cell_voxels_to_point_voxels + :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + Returns ------- pyvista.ImageData Image with voxels represented as cells. + + Examples + -------- + Create image data with eight points representing eight point voxels. + + >>> import pyvista as pv + >>> voxel_points = pv.ImageData(dimensions=(2, 2, 2)) + >>> voxel_points.point_data['Data'] = range(8) + >>> voxel_points.dimensions + (2, 2, 2) + + If we plot the image, it is visually represented as a single voxel **cell** with + eight points. + + >>> voxel_points.plot(show_edges=True) + + However, this does not visually show the correct representation when point data + is used to represent the centers of voxels. In this case we can convert the + point data to cell data to create a cell-based representation of the voxels. + + >>> voxel_cells = voxel_points.point_voxels_to_cell_voxels() + >>> voxel_cells.dimensions + (3, 3, 3) + >>> voxel_cells.plot(show_edges=True) + + Show the two representations together. The cell centers of the voxel cells + correspond to the original voxel points. + + >>> # Clear scalar data for plotting + >>> voxel_points.clear_data() + >>> voxel_cells.clear_data() + >>> + >>> cell_centers = voxel_cells.cell_centers() + >>> cell_edges = voxel_cells.extract_all_edges() + >>> + >>> plot = pv.Plotter() + >>> _ = plot.add_mesh(voxel_points, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh(cell_edges, color='black') + >>> _ = plot.add_points( + ... cell_centers, + ... render_points_as_spheres=True, + ... color='red', + ... point_size=20, + ... ) + >>> _ = plot.camera.azimuth = -25 + >>> _ = plot.camera.elevation = 25 + >>> plot.show() """ return self._convert_voxels(points_to_cells=True) @@ -989,13 +1044,66 @@ def cell_voxels_to_point_voxels(self): All cell data (if any) is converted into point data. Any active cell scalars will remain active as point scalars in the output. If the input contains point - data, it is ignored and removed the output. The dimensions of the returned + data, it is ignored and removed from the output. The dimensions of the returned image are all decreased by one relative to the input dimensions. + .. versionadded:: 0.44.0 + + See Also + -------- + point_voxels_to_cell_voxels + :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + Returns ------- pyvista.ImageData Image with voxels represented as points. + + Examples + -------- + Create image data with eight voxel cells. + + >>> import pyvista as pv + >>> voxel_cells = pv.ImageData(dimensions=(3, 3, 3)) + >>> voxel_cells.cell_data['Data'] = range(8) + >>> voxel_cells.dimensions + (3, 3, 3) + + If we plot the image, it is visually represented as eight voxel cells with + 27 points. + + >>> voxel_cells.plot(show_edges=True) + + Alternatively, we can represent the voxels as eight points instead. + + >>> voxel_points = voxel_cells.cell_voxels_to_point_voxels() + >>> voxel_points.dimensions + (2, 2, 2) + >>> voxel_points.plot(show_edges=True) + + Show the two representations together. The voxel points correspond to the + cell centers of the original voxel cells. + + >>> # Clear scalar data for plotting + >>> voxel_points.clear_data() + >>> voxel_cells.clear_data() + >>> + >>> cell_centers = voxel_cells.cell_centers() + >>> cell_edges = voxel_cells.extract_all_edges() + >>> + >>> plot = pv.Plotter() + >>> _ = plot.add_mesh(voxel_points, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh(cell_edges, color='black') + >>> _ = plot.add_points( + ... cell_centers, + ... render_points_as_spheres=True, + ... color='red', + ... point_size=20, + ... ) + >>> _ = plot.camera.azimuth = -25 + >>> _ = plot.camera.elevation = 25 + >>> plot.show() """ return self._convert_voxels(points_to_cells=False) From a69c6356e9d98338e041ea9b9336679aaabd887a Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Sat, 11 May 2024 16:02:26 -0600 Subject: [PATCH 04/19] Add copy parameter; update docstrings --- pyvista/core/filters/image_data.py | 79 ++++++++++++++++++++-------- tests/core/test_imagedata_filters.py | 34 ++++++++++-- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 59d7bac952..9838488be5 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -951,31 +951,47 @@ def contour_labeled( _vtk.vtkLogger.SetStderrVerbosity(verbosity) return cast(pyvista.PolyData, wrap(alg.GetOutput())) - def point_voxels_to_cell_voxels(self): + def point_voxels_to_cell_voxels(self, copy: bool = True): """Convert point voxel data to cell voxel data. Convert voxel data represented as points in a uniform grid into voxel cells in a uniform grid. The conversion is performed such that the input points have the same world coordinates as the centers of the converted voxel cells. - Since many filters are inherently either point filters (e.g. ImageDataFilters) - or cell filters (e.g. DataSetFilters), this conversion enables point voxel data - to be used with cell-based filters while ensuring the voxels have the - appropriate representation. + All point data at the input (if any) is passed through unmodified and associated + with cell data at the output. The data arrays are copied by default, but this + can be disabled to allow both the input (point voxels) and output (cell voxels) + to refer to the same data array(s) in memory. Any cell data at the input is + ignored and is not used. - All point data (if any) is converted into cell data. Any active point scalars - will remain active as cell scalars in the output. If the input contains cell - data, it is ignored and removed from the output. The dimensions of the returned - image are all increased by one relative to the input dimensions. + The dimensions of the returned image are all increased by one relative to the + input dimensions. The number of cells at the output equals the number of points + at the input. + + Since many filters operate on point data exclusively or are inherently cell-based, + this conversion enables the same data to be used with either kind of filter while + ensuring the input data to those filters has the appropriate representation of + the voxels. .. versionadded:: 0.44.0 + .. note:: + This function can be used with any :class:`pyvista.ImageData`, not just + 3D image data. For example, it can also be used to convert 2D point pixels + to cell pixels. + See Also -------- cell_voxels_to_point_voxels :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + Parameters + ---------- + copy : bool, default: True + Copy the input point data before associating it with the output cell data. + If ``False``, the input and output will both refer to the same data arrays. + Returns ------- pyvista.ImageData @@ -1028,33 +1044,49 @@ def point_voxels_to_cell_voxels(self): >>> _ = plot.camera.elevation = 25 >>> plot.show() """ - return self._convert_voxels(points_to_cells=True) + return self._convert_voxels(points_to_cells=True, copy=copy) - def cell_voxels_to_point_voxels(self): + def cell_voxels_to_point_voxels(self, copy: bool = True): """Convert cell voxel data to point voxel data. Convert voxel data represented as voxel cells in a uniform grid into points in a uniform grid. The conversion is performed such that the centers of the input voxel cells have the same world coordinates as the converted points. - Since many filters are inherently either point filters (e.g. ImageDataFilters) - or cell filters (e.g. DataSetFilters), this conversion enables point voxel data - to be used with cell-based filters while ensuring the voxels have the - appropriate representation. + All cell data at the input (if any) is passed through unmodified and associated + with point data at the output. The data arrays are copied by default, but this + can be disabled to allow both the input (cell voxels) and output (point voxels) + to refer to the same data array(s) in memory. Any point data at the input is + ignored and is not used. + + The dimensions of the returned image are all decreased by one relative to the + input dimensions. The number of points at the output equals the number of cells + at the input. - All cell data (if any) is converted into point data. Any active cell scalars - will remain active as point scalars in the output. If the input contains point - data, it is ignored and removed from the output. The dimensions of the returned - image are all decreased by one relative to the input dimensions. + Since many filters are inherently cell-based or operate on point data exclusively, + this conversion enables the same data to be used with either kind of filter while + ensuring the input data to those filters has the appropriate representation of + the voxels. .. versionadded:: 0.44.0 + .. note:: + This function can be used with any :class:`pyvista.ImageData`, not just + 3D image data. For example, it can also be used to convert 2D point pixels + to cell pixels. + See Also -------- point_voxels_to_cell_voxels :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + Parameters + ---------- + copy : bool, default: True + Copy the input cell data before associating it with the output point data. + If ``False``, the input and output will both refer to the same data arrays. + Returns ------- pyvista.ImageData @@ -1105,9 +1137,9 @@ def cell_voxels_to_point_voxels(self): >>> _ = plot.camera.elevation = 25 >>> plot.show() """ - return self._convert_voxels(points_to_cells=False) + return self._convert_voxels(points_to_cells=False, copy=copy) - def _convert_voxels(self, points_to_cells: bool): + def _convert_voxels(self, points_to_cells: bool, copy: bool): """Convert point voxels to cell voxels or vice-versa. If there are active scalars for the input voxels, they will be set to active @@ -1121,6 +1153,9 @@ def _convert_voxels(self, points_to_cells: bool): Set to ``True`` to convert point voxels to cell voxels. Set to ``False`` to convert cell voxels to point voxels. + copy : bool + Copy the input data before associating it with the output data. + Returns ------- pyvista.ImageData @@ -1173,7 +1208,7 @@ def _get_output_scalars(preference): # Copy old data (point or cell) to new data (cell or point) for array_name in old_data.keys(): - new_data[array_name] = old_data[array_name] + new_data[array_name] = old_data[array_name].copy() if copy else old_data[array_name] new_image.set_active_scalars(output_scalars) return new_image diff --git a/tests/core/test_imagedata_filters.py b/tests/core/test_imagedata_filters.py index 6b2bb09a81..92b6d2f092 100644 --- a/tests/core/test_imagedata_filters.py +++ b/tests/core/test_imagedata_filters.py @@ -136,42 +136,68 @@ def uniform_many_scalars(uniform): return uniform +@pytest.mark.parametrize('copy', [True, False]) @pytest.mark.parametrize( 'active_scalars', [None, 'Spatial Point Data2', 'Spatial Point Data'], ) -def test_point_voxels_to_cell_voxels(uniform_many_scalars, active_scalars): +def test_point_voxels_to_cell_voxels(uniform_many_scalars, active_scalars, copy): uniform_many_scalars.set_active_scalars(active_scalars) point_voxel_image = uniform_many_scalars point_voxel_points = point_voxel_image.points - cell_voxel_image = point_voxel_image.point_voxels_to_cell_voxels() + cell_voxel_image = point_voxel_image.point_voxels_to_cell_voxels(copy=copy) cell_voxel_center_points = cell_voxel_image.cell_centers().points + assert point_voxel_image.n_points == cell_voxel_image.n_cells assert cell_voxel_image.active_scalars_name == active_scalars assert set(cell_voxel_image.array_names) == {'Spatial Point Data', 'Spatial Point Data2'} assert np.array_equal(point_voxel_points, cell_voxel_center_points) assert np.array_equal(point_voxel_image.active_scalars, cell_voxel_image.active_scalars) + assert cell_voxel_image.point_data.keys() == [] + for array_in, array_out in zip( + point_voxel_image.point_data.keys(), + cell_voxel_image.cell_data.keys(), + ): + shares_memory = np.shares_memory(point_voxel_image[array_in], cell_voxel_image[array_out]) + if copy: + assert not shares_memory + else: + assert shares_memory + +@pytest.mark.parametrize('copy', [True, False]) @pytest.mark.parametrize( 'active_scalars', [None, 'Spatial Cell Data2', 'Spatial Cell Data'], ) -def test_cell_voxels_to_point_voxels(uniform_many_scalars, active_scalars): +def test_cell_voxels_to_point_voxels(uniform_many_scalars, active_scalars, copy): uniform_many_scalars.set_active_scalars(active_scalars) cell_voxel_image = uniform_many_scalars cell_voxel_center_points = cell_voxel_image.cell_centers().points - point_voxel_image = cell_voxel_image.cell_voxels_to_point_voxels() + point_voxel_image = cell_voxel_image.cell_voxels_to_point_voxels(copy=copy) point_voxel_points = point_voxel_image.points + assert cell_voxel_image.n_cells == point_voxel_image.n_points assert cell_voxel_image.active_scalars_name == active_scalars assert set(point_voxel_image.array_names) == {'Spatial Cell Data', 'Spatial Cell Data2'} assert np.array_equal(cell_voxel_center_points, point_voxel_points) assert np.array_equal(cell_voxel_image.active_scalars, point_voxel_image.active_scalars) + assert point_voxel_image.cell_data.keys() == [] + + for array_in, array_out in zip( + cell_voxel_image.cell_data.keys(), + point_voxel_image.point_data.keys(), + ): + shares_memory = np.shares_memory(cell_voxel_image[array_in], point_voxel_image[array_out]) + if copy: + assert not shares_memory + else: + assert shares_memory @pytest.fixture() From ba54730a9b42b33a86c35727da8f4f4fc6f700c4 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Sat, 11 May 2024 16:04:46 -0600 Subject: [PATCH 05/19] Fix typo --- pyvista/core/filters/image_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 9838488be5..7a6fbc9f6f 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -1072,8 +1072,8 @@ def cell_voxels_to_point_voxels(self, copy: bool = True): .. note:: This function can be used with any :class:`pyvista.ImageData`, not just - 3D image data. For example, it can also be used to convert 2D point pixels - to cell pixels. + 3D image data. For example, it can also be used to convert 2D cell pixels + to point pixels. See Also -------- From a4b2410b61003e1c286df0733884e71001f902d4 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Tue, 14 May 2024 00:24:39 -0600 Subject: [PATCH 06/19] Rename filters, update docstrings, add scalars param --- pyvista/core/filters/image_data.py | 290 ++++++++++++++++++--------- tests/core/test_dataobject.py | 4 +- tests/core/test_imagedata_filters.py | 32 ++- 3 files changed, 220 insertions(+), 106 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 7a6fbc9f6f..e46c0eea5a 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -951,88 +951,97 @@ def contour_labeled( _vtk.vtkLogger.SetStderrVerbosity(verbosity) return cast(pyvista.PolyData, wrap(alg.GetOutput())) - def point_voxels_to_cell_voxels(self, copy: bool = True): - """Convert point voxel data to cell voxel data. - - Convert voxel data represented as points in a uniform grid into voxel cells - in a uniform grid. The conversion is performed such that the input points have - the same world coordinates as the centers of the converted voxel cells. - - All point data at the input (if any) is passed through unmodified and associated - with cell data at the output. The data arrays are copied by default, but this - can be disabled to allow both the input (point voxels) and output (cell voxels) - to refer to the same data array(s) in memory. Any cell data at the input is - ignored and is not used. - - The dimensions of the returned image are all increased by one relative to the - input dimensions. The number of cells at the output equals the number of points - at the input. - - Since many filters operate on point data exclusively or are inherently cell-based, - this conversion enables the same data to be used with either kind of filter while - ensuring the input data to those filters has the appropriate representation of - the voxels. + def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): + """Convert image data from a points-based to a cells-based representation. + + Convert an image represented as points with point data into an alternative + representation using cells with cell data. The conversion is lossless in the + sense that point data at the input is passed through unmodified and stored as + cell data at the output. + + The main effect of this filter is to transform the :class:`~pyvista.ImageData` + container itself. The input points are used to represent the centers of the + output cells, which has the effect of "growing" the input image dimensions by + one along each axis (i.e. 1/2 cell width on each side). For example, an image + with 100 points and 99 cells along an axis at the input will have 101 points and + 100 cells at the output; if the input has 1mm spacing, the axis size is increased + from 99mm to 100mm. + + Since many filters are inherently cell-based or may operate on point data + exclusively, this conversion enables the same data to be used with either kind + of filter while ensuring the input data to those filters has the appropriate + representation of the voxels. It may also be useful for plotting to achieve + a desired visual effect. .. versionadded:: 0.44.0 - .. note:: - This function can be used with any :class:`pyvista.ImageData`, not just - 3D image data. For example, it can also be used to convert 2D point pixels - to cell pixels. - See Also -------- - cell_voxels_to_point_voxels - :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + cells_to_points :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` Parameters ---------- + scalars : str, optional + Name of point data scalars to pass through to the output as cell data. Use + this parameter to restrict the output to only include the specified array. + By default, all point data arrays are passed through as cell data. + copy : bool, default: True Copy the input point data before associating it with the output cell data. - If ``False``, the input and output will both refer to the same data arrays. + If ``False``, the input and output will both refer to the same data array(s). Returns ------- pyvista.ImageData - Image with voxels represented as cells. + Image with a cells-based representation. Examples -------- - Create image data with eight points representing eight point voxels. + Create image data with eight points representing eight voxels. >>> import pyvista as pv - >>> voxel_points = pv.ImageData(dimensions=(2, 2, 2)) - >>> voxel_points.point_data['Data'] = range(8) - >>> voxel_points.dimensions + >>> points_image = pv.ImageData(dimensions=(2, 2, 2)) + >>> points_image.point_data['Data'] = range(8) + >>> points_image.n_points + 8 + >>> points_image.n_cells + 1 + >>> points_image.dimensions (2, 2, 2) - If we plot the image, it is visually represented as a single voxel **cell** with + If we plot the image, it is visually represented as a single cell with eight points. - >>> voxel_points.plot(show_edges=True) + >>> points_image.plot(show_edges=True) - However, this does not visually show the correct representation when point data - is used to represent the centers of voxels. In this case we can convert the - point data to cell data to create a cell-based representation of the voxels. + However, this does not show the correct representation of our eight input points + when the point samples are used to represent the center-points of voxels. In + this case we can convert the point data to cell data to create a cell-based + representation of the image. - >>> voxel_cells = voxel_points.point_voxels_to_cell_voxels() - >>> voxel_cells.dimensions + >>> cells_image = points_image.points_to_cells() + >>> cells_image.n_points + 27 + >>> cells_image.n_cells + 8 + >>> cells_image.dimensions (3, 3, 3) - >>> voxel_cells.plot(show_edges=True) + >>> cells_image.plot(show_edges=True) Show the two representations together. The cell centers of the voxel cells correspond to the original voxel points. >>> # Clear scalar data for plotting - >>> voxel_points.clear_data() - >>> voxel_cells.clear_data() + >>> points_image.clear_data() + >>> cells_image.clear_data() >>> - >>> cell_centers = voxel_cells.cell_centers() - >>> cell_edges = voxel_cells.extract_all_edges() + >>> cell_centers = cells_image.cell_centers() + >>> cell_edges = cells_image.extract_all_edges() >>> >>> plot = pv.Plotter() - >>> _ = plot.add_mesh(voxel_points, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) >>> _ = plot.add_mesh(cell_edges, color='black') >>> _ = plot.add_points( ... cell_centers, @@ -1044,88 +1053,159 @@ def point_voxels_to_cell_voxels(self, copy: bool = True): >>> _ = plot.camera.elevation = 25 >>> plot.show() """ - return self._convert_voxels(points_to_cells=True, copy=copy) - - def cell_voxels_to_point_voxels(self, copy: bool = True): - """Convert cell voxel data to point voxel data. - - Convert voxel data represented as voxel cells in a uniform grid into points - in a uniform grid. The conversion is performed such that the centers of the - input voxel cells have the same world coordinates as the converted points. - - All cell data at the input (if any) is passed through unmodified and associated - with point data at the output. The data arrays are copied by default, but this - can be disabled to allow both the input (cell voxels) and output (point voxels) - to refer to the same data array(s) in memory. Any point data at the input is - ignored and is not used. - - The dimensions of the returned image are all decreased by one relative to the - input dimensions. The number of points at the output equals the number of cells - at the input. - - Since many filters are inherently cell-based or operate on point data exclusively, - this conversion enables the same data to be used with either kind of filter while - ensuring the input data to those filters has the appropriate representation of - the voxels. + if scalars is not None: + field = self.get_array_association(scalars, preference='point') # type: ignore[attr-defined] + if field != FieldAssociation.POINT: + raise ValueError( + f"Scalars '{scalars}' must be associated with point data. Got {field.name.lower()} data instead.", + ) + return self._convert_points_cells(points_to_cells=True, scalars=scalars, copy=copy) + + def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): + """Convert image data from a cells-based to a points-based representation. + + Convert an image represented as cells with cell data into an alternative + representation using points with point data. The conversion is lossless in the + sense that cell data at the input is passed through unmodified and stored as + point data at the output. + + The main effect of this filter is to transform the :class:`~pyvista.ImageData` + container itself. The input cell centers are used to represent the output points, + which has the effect of "shrinking" the input image dimensions by one along each + axis (i.e. 1/2 cell width on each side). For example, an image with 101 points + and 100 cells along an axis at the input will have 100 points and 99 cells at + the output; if the input has 1mm spacing, the axis size is reduced from 100mm + to 99mm. + + Since many filters are inherently cell-based or may operate on point data + exclusively, this conversion enables the same data to be used with either kind + of filter while ensuring the input data to those filters has the appropriate + representation of the voxels. It may also be useful for plotting to achieve + a desired visual effect. .. versionadded:: 0.44.0 - .. note:: - This function can be used with any :class:`pyvista.ImageData`, not just - 3D image data. For example, it can also be used to convert 2D cell pixels - to point pixels. - See Also -------- - point_voxels_to_cell_voxels - :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + points_to_cells :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` Parameters ---------- + scalars : str, optional + Name of cell data scalars to pass through to the output as point data. Use + this parameter to restrict the output to only include the specified array. + By default, all cell data arrays are passed through as point data. + copy : bool, default: True Copy the input cell data before associating it with the output point data. - If ``False``, the input and output will both refer to the same data arrays. + If ``False``, the input and output will both refer to the same data array(s). Returns ------- pyvista.ImageData - Image with voxels represented as points. + Image with a points-based representation. Examples -------- - Create image data with eight voxel cells. + Create image data with eight cells representing eight voxels. >>> import pyvista as pv - >>> voxel_cells = pv.ImageData(dimensions=(3, 3, 3)) - >>> voxel_cells.cell_data['Data'] = range(8) - >>> voxel_cells.dimensions + >>> cells_image = pv.ImageData(dimensions=(3, 3, 3)) + >>> cells_image.cell_data['Data'] = range(8) + >>> cells_image.n_points + 27 + >>> cells_image.n_cells + 8 + >>> cells_image.dimensions (3, 3, 3) If we plot the image, it is visually represented as eight voxel cells with 27 points. - >>> voxel_cells.plot(show_edges=True) + >>> cells_image.plot(show_edges=True) Alternatively, we can represent the voxels as eight points instead. - >>> voxel_points = voxel_cells.cell_voxels_to_point_voxels() - >>> voxel_points.dimensions + >>> points_image = cells_image.cells_to_points() + >>> points_image.n_points + 8 + >>> points_image.n_cells + 1 + >>> points_image.dimensions (2, 2, 2) - >>> voxel_points.plot(show_edges=True) + >>> points_image.plot(show_edges=True) - Show the two representations together. The voxel points correspond to the - cell centers of the original voxel cells. + Show the two representations together. The points of the points image correspond + to the cell centers of the cells image. >>> # Clear scalar data for plotting - >>> voxel_points.clear_data() - >>> voxel_cells.clear_data() + >>> points_image.clear_data() + >>> cells_image.clear_data() >>> - >>> cell_centers = voxel_cells.cell_centers() - >>> cell_edges = voxel_cells.extract_all_edges() + >>> cell_centers = cells_image.cell_centers() + >>> cell_edges = cells_image.extract_all_edges() >>> >>> plot = pv.Plotter() - >>> _ = plot.add_mesh(voxel_points, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh(cell_edges, color='black') + >>> _ = plot.add_points( + ... cell_centers, + ... render_points_as_spheres=True, + ... color='red', + ... point_size=20, + ... ) + >>> _ = plot.camera.azimuth = -25 + >>> _ = plot.camera.elevation = 25 + >>> plot.show() + + + + + Create image data with eight points representing eight voxels. + + >>> import pyvista as pv + >>> points_image = pv.ImageData(dimensions=(2, 2, 2)) + >>> points_image.point_data['Data'] = range(8) + >>> points_image.n_points + 8 + >>> points_image.n_cells + 1 + >>> points_image.dimensions + (2, 2, 2) + + If we plot the image, it is visually represented as a single cell with + eight points. + + >>> points_image.plot(show_edges=True) + + However, this does not show the correct representation of our eight input points + when the point samples are used to represent the center-points of voxels. In + this case we can convert the point data to cell data to create a cell-based + representation of the image. + + >>> cells_image = points_image.points_to_cells() + >>> cells_image.n_points + 27 + >>> cells_image.n_cells + 8 + >>> cells_image.dimensions + (3, 3, 3) + >>> cells_image.plot(show_edges=True) + + Show the two representations together. The points of the points image correspond + to the cell centers of the cells image. + + >>> # Clear scalar data for plotting + >>> points_image.clear_data() + >>> cells_image.clear_data() + >>> + >>> cell_centers = cells_image.cell_centers() + >>> cell_edges = cells_image.extract_all_edges() + >>> + >>> plot = pv.Plotter() + >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) >>> _ = plot.add_mesh(cell_edges, color='black') >>> _ = plot.add_points( ... cell_centers, @@ -1137,10 +1217,16 @@ def cell_voxels_to_point_voxels(self, copy: bool = True): >>> _ = plot.camera.elevation = 25 >>> plot.show() """ - return self._convert_voxels(points_to_cells=False, copy=copy) + if scalars is not None: + field = self.get_array_association(scalars, preference='cell') # type: ignore[attr-defined] + if field != FieldAssociation.CELL: + raise ValueError( + f"Scalars '{scalars}' must be associated with cell data. Got {field.name.lower()} data instead.", + ) + return self._convert_points_cells(points_to_cells=False, scalars=scalars, copy=copy) - def _convert_voxels(self, points_to_cells: bool, copy: bool): - """Convert point voxels to cell voxels or vice-versa. + def _convert_points_cells(self, points_to_cells: bool, scalars: Optional[str], copy: bool): + """Convert points to cells or vice-versa. If there are active scalars for the input voxels, they will be set to active for the output voxels. For example, if converting point voxels to cell voxels, @@ -1153,6 +1239,9 @@ def _convert_voxels(self, points_to_cells: bool, copy: bool): Set to ``True`` to convert point voxels to cell voxels. Set to ``False`` to convert cell voxels to point voxels. + scalars : str + If set, only these scalars are passed through. + copy : bool Copy the input data before associating it with the output data. @@ -1178,14 +1267,14 @@ def _get_output_scalars(preference): # Get data to use and operations to perform for the conversion new_image = pyvista.ImageData() if points_to_cells: - output_scalars = _get_output_scalars('point') + output_scalars = scalars if scalars else _get_output_scalars('point') # Enlarge image so points become cell centers origin_operator = operator.sub dims_operator = operator.add # Increase dimensions old_data = point_data new_data = new_image.cell_data else: # cells_to_points - output_scalars = _get_output_scalars('cell') + output_scalars = scalars if scalars else _get_output_scalars('cell') # Shrink image so cell centers become points origin_operator = operator.add dims_operator = operator.sub # Decrease dimensions @@ -1207,7 +1296,8 @@ def _get_output_scalars(preference): new_image.field_data.update(self.field_data) # type: ignore[attr-defined] # Copy old data (point or cell) to new data (cell or point) - for array_name in old_data.keys(): + array_names = [scalars] if scalars else old_data.keys() + for array_name in array_names: new_data[array_name] = old_data[array_name].copy() if copy else old_data[array_name] new_image.set_active_scalars(output_scalars) diff --git a/tests/core/test_dataobject.py b/tests/core/test_dataobject.py index 15a97879dc..5833c5300c 100644 --- a/tests/core/test_dataobject.py +++ b/tests/core/test_dataobject.py @@ -200,11 +200,11 @@ def test_user_dict_persists_with_pack_labels_filter(): def test_user_dict_persists_with_point_voxels_to_cell_voxels(uniform): uniform.user_dict['name'] = 'image' - uniform.cell_voxels_to_point_voxels() + uniform.cells_to_points() assert uniform.user_dict['name'] == 'image' def test_user_dict_persists_with_cell_voxels_to_point_voxels(uniform): uniform.user_dict['name'] = 'image' - uniform.point_voxels_to_cell_voxels() + uniform.points_to_cells() assert uniform.user_dict['name'] == 'image' diff --git a/tests/core/test_imagedata_filters.py b/tests/core/test_imagedata_filters.py index 92b6d2f092..49f05eee5b 100644 --- a/tests/core/test_imagedata_filters.py +++ b/tests/core/test_imagedata_filters.py @@ -141,13 +141,13 @@ def uniform_many_scalars(uniform): 'active_scalars', [None, 'Spatial Point Data2', 'Spatial Point Data'], ) -def test_point_voxels_to_cell_voxels(uniform_many_scalars, active_scalars, copy): +def test_points_to_cells(uniform_many_scalars, active_scalars, copy): uniform_many_scalars.set_active_scalars(active_scalars) point_voxel_image = uniform_many_scalars point_voxel_points = point_voxel_image.points - cell_voxel_image = point_voxel_image.point_voxels_to_cell_voxels(copy=copy) + cell_voxel_image = point_voxel_image.points_to_cells(copy=copy) cell_voxel_center_points = cell_voxel_image.cell_centers().points assert point_voxel_image.n_points == cell_voxel_image.n_cells @@ -173,13 +173,13 @@ def test_point_voxels_to_cell_voxels(uniform_many_scalars, active_scalars, copy) 'active_scalars', [None, 'Spatial Cell Data2', 'Spatial Cell Data'], ) -def test_cell_voxels_to_point_voxels(uniform_many_scalars, active_scalars, copy): +def test_cells_to_points(uniform_many_scalars, active_scalars, copy): uniform_many_scalars.set_active_scalars(active_scalars) cell_voxel_image = uniform_many_scalars cell_voxel_center_points = cell_voxel_image.cell_centers().points - point_voxel_image = cell_voxel_image.cell_voxels_to_point_voxels(copy=copy) + point_voxel_image = cell_voxel_image.cells_to_points(copy=copy) point_voxel_points = point_voxel_image.points assert cell_voxel_image.n_cells == point_voxel_image.n_points @@ -200,6 +200,30 @@ def test_cell_voxels_to_point_voxels(uniform_many_scalars, active_scalars, copy) assert shares_memory +def test_points_to_cells_scalars(uniform): + scalars = 'Spatial Point Data' + converted = uniform.points_to_cells(scalars) + assert converted.active_scalars_name == scalars + assert converted.cell_data.keys() == [scalars] + + match = "Scalars 'Spatial Cell Data' must be associated with point data. Got cell data instead." + with pytest.raises(ValueError, match=match): + uniform.points_to_cells('Spatial Cell Data') + + +def test_cells_to_points_scalars(uniform): + scalars = 'Spatial Cell Data' + converted = uniform.cells_to_points(scalars) + assert converted.active_scalars_name == scalars + assert converted.point_data.keys() == [scalars] + + match = ( + "Scalars 'Spatial Point Data' must be associated with cell data. Got point data instead." + ) + with pytest.raises(ValueError, match=match): + uniform.cells_to_points('Spatial Point Data') + + @pytest.fixture() def single_point_image(): image = pv.ImageData(dimensions=(1, 1, 1)) From 94dc32c2a7e3d1a3cd38760fa65753625bcf9110 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Tue, 14 May 2024 00:30:28 -0600 Subject: [PATCH 07/19] Remove copied text --- pyvista/core/filters/image_data.py | 57 ------------------------------ 1 file changed, 57 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index e46c0eea5a..6d7c6156f9 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -1140,63 +1140,6 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): Show the two representations together. The points of the points image correspond to the cell centers of the cells image. - >>> # Clear scalar data for plotting - >>> points_image.clear_data() - >>> cells_image.clear_data() - >>> - >>> cell_centers = cells_image.cell_centers() - >>> cell_edges = cells_image.extract_all_edges() - >>> - >>> plot = pv.Plotter() - >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) - >>> _ = plot.add_mesh(cell_edges, color='black') - >>> _ = plot.add_points( - ... cell_centers, - ... render_points_as_spheres=True, - ... color='red', - ... point_size=20, - ... ) - >>> _ = plot.camera.azimuth = -25 - >>> _ = plot.camera.elevation = 25 - >>> plot.show() - - - - - Create image data with eight points representing eight voxels. - - >>> import pyvista as pv - >>> points_image = pv.ImageData(dimensions=(2, 2, 2)) - >>> points_image.point_data['Data'] = range(8) - >>> points_image.n_points - 8 - >>> points_image.n_cells - 1 - >>> points_image.dimensions - (2, 2, 2) - - If we plot the image, it is visually represented as a single cell with - eight points. - - >>> points_image.plot(show_edges=True) - - However, this does not show the correct representation of our eight input points - when the point samples are used to represent the center-points of voxels. In - this case we can convert the point data to cell data to create a cell-based - representation of the image. - - >>> cells_image = points_image.points_to_cells() - >>> cells_image.n_points - 27 - >>> cells_image.n_cells - 8 - >>> cells_image.dimensions - (3, 3, 3) - >>> cells_image.plot(show_edges=True) - - Show the two representations together. The points of the points image correspond - to the cell centers of the cells image. - >>> # Clear scalar data for plotting >>> points_image.clear_data() >>> cells_image.clear_data() From 34659c7156b61eca97e0e0c9d168b02e3d68bbea Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Wed, 15 May 2024 20:12:58 -0600 Subject: [PATCH 08/19] Update docs --- pyvista/core/filters/data_set.py | 18 ++++-- pyvista/core/filters/image_data.py | 98 +++++++++++++++++------------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index 02d8357288..33b7c41e24 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -3163,8 +3163,6 @@ def cell_data_to_point_data(self, pass_cell_data=False, progress_bar=False): values of all cells using a particular point. Optionally, the input cell data can be passed through to the output as well. - See also :func:`pyvista.DataSetFilters.point_data_to_cell_data`. - Parameters ---------- pass_cell_data : bool, default: False @@ -3179,6 +3177,13 @@ def cell_data_to_point_data(self, pass_cell_data=False, progress_bar=False): Dataset with the point data transformed into cell data. Return type matches input. + See Also + -------- + point_data_to_cell_data + Similar transformation applied to point data. + :meth:`~pyvista.ImageDataFilters.cells_to_points` + Re-mesh :class:`~pyvista.ImageData` to a points-based representation. + Examples -------- First compute the face area of the example airplane mesh and @@ -3249,8 +3254,6 @@ def point_data_to_cell_data(self, pass_point_data=False, progress_bar=False): Point data are specified per node and cell data specified within cells. Optionally, the input point data can be passed through to the output. - See also: :func:`pyvista.DataSetFilters.cell_data_to_point_data` - Parameters ---------- pass_point_data : bool, default: False @@ -3265,6 +3268,13 @@ def point_data_to_cell_data(self, pass_point_data=False, progress_bar=False): Dataset with the point data transformed into cell data. Return type matches input. + See Also + -------- + cell_data_to_point_data + Similar transformation applied to cell data. + :meth:`~pyvista.ImageDataFilters.points_to_cells` + Re-mesh :class:`~pyvista.ImageData` to a cells-based representation. + Examples -------- Color cells by their z coordinates. First, create point diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 6d7c6156f9..e7c502ef9c 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -952,41 +952,48 @@ def contour_labeled( return cast(pyvista.PolyData, wrap(alg.GetOutput())) def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): - """Convert image data from a points-based to a cells-based representation. - - Convert an image represented as points with point data into an alternative - representation using cells with cell data. The conversion is lossless in the - sense that point data at the input is passed through unmodified and stored as - cell data at the output. - - The main effect of this filter is to transform the :class:`~pyvista.ImageData` - container itself. The input points are used to represent the centers of the - output cells, which has the effect of "growing" the input image dimensions by - one along each axis (i.e. 1/2 cell width on each side). For example, an image - with 100 points and 99 cells along an axis at the input will have 101 points and - 100 cells at the output; if the input has 1mm spacing, the axis size is increased - from 99mm to 100mm. - - Since many filters are inherently cell-based or may operate on point data - exclusively, this conversion enables the same data to be used with either kind - of filter while ensuring the input data to those filters has the appropriate - representation of the voxels. It may also be useful for plotting to achieve - a desired visual effect. + """Re-mesh image data from a points-based to a cells-based representation. + + This filter changes how image data is represented. Data represented as points + at the input is re-meshed into an alternative representation as cells at the + output. Only the :class:`~pyvista.ImageData` container is modified so that + the number of input points equals the number of output cells. The re-meshing is + otherwise lossless in the sense that point data at the input is passed through + unmodified and stored as cell data at the output. Any cell data at the input is + ignored and is not used by this filter. + + To change the image data's representation, the input points are used to + represent the centers of the output cells. This has the effect of "growing" the + input image dimensions by one along each axis (i.e. half the cell width on each + side). For example, an image with 100 points and 99 cells along an axis at the + input will have 101 points and 100 cells at the output. If the input has 1mm + spacing, the axis size will also increase from 99mm to 100mm. + + Since filters may be inherently cell-based (e.g. some :class:`~pyvista.DataSetFilters`) + or may operate on point data exclusively (e.g. most :class:`~pyvista.ImageDataFilters`), + re-meshing enables the same data to be used with either kind of filter while + ensuring the input data to those filters has the appropriate representation. + This filter is also useful when plotting image data to achieve a desired visual + effect, such as plotting images as voxel cells instead of as points. .. versionadded:: 0.44.0 See Also -------- cells_to_points + Apply the inverse re-meshing operation to this filter. :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + Resample point data as cell data without modifying the container. :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + Resample cell data as point data without modifying the container. Parameters ---------- scalars : str, optional Name of point data scalars to pass through to the output as cell data. Use this parameter to restrict the output to only include the specified array. - By default, all point data arrays are passed through as cell data. + By default, all point data arrays at the input are passed through as cell + data at the output. copy : bool, default: True Copy the input point data before associating it with the output cell data. @@ -1062,41 +1069,48 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): return self._convert_points_cells(points_to_cells=True, scalars=scalars, copy=copy) def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): - """Convert image data from a cells-based to a points-based representation. - - Convert an image represented as cells with cell data into an alternative - representation using points with point data. The conversion is lossless in the - sense that cell data at the input is passed through unmodified and stored as - point data at the output. - - The main effect of this filter is to transform the :class:`~pyvista.ImageData` - container itself. The input cell centers are used to represent the output points, - which has the effect of "shrinking" the input image dimensions by one along each - axis (i.e. 1/2 cell width on each side). For example, an image with 101 points - and 100 cells along an axis at the input will have 100 points and 99 cells at - the output; if the input has 1mm spacing, the axis size is reduced from 100mm - to 99mm. - - Since many filters are inherently cell-based or may operate on point data - exclusively, this conversion enables the same data to be used with either kind - of filter while ensuring the input data to those filters has the appropriate - representation of the voxels. It may also be useful for plotting to achieve - a desired visual effect. + """Re-mesh image data from a cells-based to a points-based representation. + + This filter changes how image data is represented. Data represented as cells + at the input is re-meshed into an alternative representation as points at the + output. Only the :class:`~pyvista.ImageData` container is modified so that + the number of input cells equals the number of output points. The re-meshing is + otherwise lossless in the sense that cell data at the input is passed through + unmodified and stored as point data at the output. Any point data at the input is + ignored and is not used by this filter. + + To change the image data's representation, the input cell centers are used to + represent the output points. This has the effect of "shrinking" the + input image dimensions by one along each axis (i.e. half the cell width on each + side). For example, an image with 101 points and 100 cells along an axis at the + input will have 100 points and 99 cells at the output. If the input has 1mm + spacing, the axis size will also decrease from 100mm to 99mm. + + Since filters may be inherently cell-based (e.g. some :class:`~pyvista.DataSetFilters`) + or may operate on point data exclusively (e.g. most :class:`~pyvista.ImageDataFilters`), + re-meshing enables the same data to be used with either kind of filter while + ensuring the input data to those filters has the appropriate representation. + This filter is also useful when plotting image data to achieve a desired visual + effect, such as plotting images as points instead of as voxel cells. .. versionadded:: 0.44.0 See Also -------- points_to_cells + Apply the inverse re-meshing operation to this filter. :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` + Resample cell data as point data without modifying the container. :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` + Resample point data as cell data without modifying the container. Parameters ---------- scalars : str, optional Name of cell data scalars to pass through to the output as point data. Use this parameter to restrict the output to only include the specified array. - By default, all cell data arrays are passed through as point data. + By default, all cell data arrays at the input are passed through as point + data at the output. copy : bool, default: True Copy the input cell data before associating it with the output point data. From e25da2c5dae0729bffa13b07ab88a47b7df9b20f Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Wed, 15 May 2024 21:43:11 -0600 Subject: [PATCH 09/19] Update tests --- tests/core/test_imagedata_filters.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/core/test_imagedata_filters.py b/tests/core/test_imagedata_filters.py index 49f05eee5b..0bbc58ae49 100644 --- a/tests/core/test_imagedata_filters.py +++ b/tests/core/test_imagedata_filters.py @@ -162,10 +162,7 @@ def test_points_to_cells(uniform_many_scalars, active_scalars, copy): cell_voxel_image.cell_data.keys(), ): shares_memory = np.shares_memory(point_voxel_image[array_in], cell_voxel_image[array_out]) - if copy: - assert not shares_memory - else: - assert shares_memory + assert not shares_memory if copy else shares_memory @pytest.mark.parametrize('copy', [True, False]) @@ -194,10 +191,7 @@ def test_cells_to_points(uniform_many_scalars, active_scalars, copy): point_voxel_image.point_data.keys(), ): shares_memory = np.shares_memory(cell_voxel_image[array_in], point_voxel_image[array_out]) - if copy: - assert not shares_memory - else: - assert shares_memory + assert not shares_memory if copy else shares_memory def test_points_to_cells_scalars(uniform): From 0726601f45a445c6efcf4c45b37c614ab918111c Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Thu, 16 May 2024 12:06:49 -0600 Subject: [PATCH 10/19] Update terminology and docstrings --- .pre-commit-config.yaml | 10 +-- pyvista/core/filters/image_data.py | 111 +++++++++++++++++++---------- tests/core/test_dataobject.py | 4 +- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e96ee89276..8e5b23206f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -119,8 +119,8 @@ repos: - id: ruff args: [--fix] - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 - hooks: - - id: prettier - types_or: [yaml, markdown, html, css, scss, javascript, json] +# - repo: https://github.com/pre-commit/mirrors-prettier +# rev: v3.1.0 +# hooks: +# - id: prettier +# types_or: [yaml, markdown, html, css, scss, javascript, json] diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index e7c502ef9c..d6da042b8d 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -981,7 +981,7 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): See Also -------- cells_to_points - Apply the inverse re-meshing operation to this filter. + Inverse of this filter to represent cells as points. :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` Resample point data as cell data without modifying the container. :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` @@ -1006,11 +1006,11 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): Examples -------- - Create image data with eight points representing eight voxels. + Create image data with eight points. >>> import pyvista as pv >>> points_image = pv.ImageData(dimensions=(2, 2, 2)) - >>> points_image.point_data['Data'] = range(8) + >>> points_image.point_data['Data'] = list(range(8))[::-1] >>> points_image.n_points 8 >>> points_image.n_cells @@ -1018,15 +1018,16 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): >>> points_image.dimensions (2, 2, 2) - If we plot the image, it is visually represented as a single cell with - eight points. + If we plot the image, it is represented as a single cell with eight points. The + point data is interpolated to color the cell. >>> points_image.plot(show_edges=True) - However, this does not show the correct representation of our eight input points - when the point samples are used to represent the center-points of voxels. In - this case we can convert the point data to cell data to create a cell-based - representation of the image. + However, in many applications (e.g. 3D medical images) the point samples are + used to represent the center-points of voxels with discrete values. As such, it + may be preferred to represent the data as eight cells instead of eight points. In + this case, we can re-mesh the point data to cell data to create a cell-based + representation. >>> cells_image = points_image.points_to_cells() >>> cells_image.n_points @@ -1035,20 +1036,22 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): 8 >>> cells_image.dimensions (3, 3, 3) + + Now, when we plot the image, we have a more appropriate representation with eight + voxel cells, and the discrete data is no longer interpolated. + >>> cells_image.plot(show_edges=True) - Show the two representations together. The cell centers of the voxel cells - correspond to the original voxel points. + Show the two representations together. The cell centers of the cells image + correspond to the points of the points image. - >>> # Clear scalar data for plotting - >>> points_image.clear_data() - >>> cells_image.clear_data() - >>> >>> cell_centers = cells_image.cell_centers() >>> cell_edges = cells_image.extract_all_edges() >>> >>> plot = pv.Plotter() - >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh( + ... points_image, color=True, show_edges=True, opacity=0.7 + ... ) >>> _ = plot.add_mesh(cell_edges, color='black') >>> _ = plot.add_points( ... cell_centers, @@ -1059,6 +1062,28 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): >>> _ = plot.camera.azimuth = -25 >>> _ = plot.camera.elevation = 25 >>> plot.show() + + With the data represented as cells, we can now use a cell-based filter such as + :meth:`~pyvista.DataSetFilters.threshold`. Since the original image data + represents discrete voxels, this filter produces the expected result. + + >>> cells_thresh = cells_image.threshold(2) + >>> cells_thresh.plot(show_edges=True) + + For comparison, apply the same filter to the points-based representation. + + >>> points_thresh = points_image.threshold(2) + >>> points_thresh.plot(show_edges=True) + + We can see that this produces a very different result. In fact, since the input + only has a single cell, using the cell-based filter with the points-based + representation had no effect on the scalar data. + + >>> points_thresh['Data'] + pyvista_ndarray([7, 6, 5, 4, 3, 2, 1, 0]) + >>> points_image['Data'] + pyvista_ndarray([7, 6, 5, 4, 3, 2, 1, 0]) + """ if scalars is not None: field = self.get_array_association(scalars, preference='point') # type: ignore[attr-defined] @@ -1066,7 +1091,7 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): raise ValueError( f"Scalars '{scalars}' must be associated with point data. Got {field.name.lower()} data instead.", ) - return self._convert_points_cells(points_to_cells=True, scalars=scalars, copy=copy) + return self._remesh_points_cells(points_to_cells=True, scalars=scalars, copy=copy) def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): """Re-mesh image data from a cells-based to a points-based representation. @@ -1098,7 +1123,7 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): See Also -------- points_to_cells - Apply the inverse re-meshing operation to this filter. + Inverse of this filter to represent points as cells. :meth:`~pyvista.DataSetFilters.cell_data_to_point_data` Resample cell data as point data without modifying the container. :meth:`~pyvista.DataSetFilters.point_data_to_cell_data` @@ -1123,11 +1148,11 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): Examples -------- - Create image data with eight cells representing eight voxels. + Create image data with eight cells. >>> import pyvista as pv >>> cells_image = pv.ImageData(dimensions=(3, 3, 3)) - >>> cells_image.cell_data['Data'] = range(8) + >>> cells_image.cell_data['Data'] = list(range(8))[::-1] >>> cells_image.n_points 27 >>> cells_image.n_cells @@ -1135,12 +1160,11 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): >>> cells_image.dimensions (3, 3, 3) - If we plot the image, it is visually represented as eight voxel cells with - 27 points. + If we plot the image, it is represented as eight cells with 27 points. >>> cells_image.plot(show_edges=True) - Alternatively, we can represent the voxels as eight points instead. + Alternatively, we can represent the cells as eight points instead. >>> points_image = cells_image.cells_to_points() >>> points_image.n_points @@ -1154,15 +1178,13 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): Show the two representations together. The points of the points image correspond to the cell centers of the cells image. - >>> # Clear scalar data for plotting - >>> points_image.clear_data() - >>> cells_image.clear_data() - >>> >>> cell_centers = cells_image.cell_centers() >>> cell_edges = cells_image.extract_all_edges() >>> >>> plot = pv.Plotter() - >>> _ = plot.add_mesh(points_image, show_edges=True, opacity=0.7) + >>> _ = plot.add_mesh( + ... points_image, color=True, show_edges=True, opacity=0.7 + ... ) >>> _ = plot.add_mesh(cell_edges, color='black') >>> _ = plot.add_points( ... cell_centers, @@ -1173,6 +1195,21 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): >>> _ = plot.camera.azimuth = -25 >>> _ = plot.camera.elevation = 25 >>> plot.show() + + With a points-based representation, we can now use a points-based filter with + our dataset such as :meth:`~pyvista.ImageDataFilters.image_threshold`. + + >>> points_thresh = points_image.image_threshold(2) + + The filter works as expected, but when we plot it the values are interpolated. + + >>> points_thresh.plot(show_edges=True) + + Convert the result back to a cell representation to better visualize the result. + + >>> cells_thresh = points_thresh.points_to_cells() + >>> cells_thresh.plot(show_edges=True) + """ if scalars is not None: field = self.get_array_association(scalars, preference='cell') # type: ignore[attr-defined] @@ -1180,21 +1217,19 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): raise ValueError( f"Scalars '{scalars}' must be associated with cell data. Got {field.name.lower()} data instead.", ) - return self._convert_points_cells(points_to_cells=False, scalars=scalars, copy=copy) + return self._remesh_points_cells(points_to_cells=False, scalars=scalars, copy=copy) - def _convert_points_cells(self, points_to_cells: bool, scalars: Optional[str], copy: bool): - """Convert points to cells or vice-versa. + def _remesh_points_cells(self, points_to_cells: bool, scalars: Optional[str], copy: bool): + """Re-mesh points to cells or vice-versa. - If there are active scalars for the input voxels, they will be set to active - for the output voxels. For example, if converting point voxels to cell voxels, - and the input has active point scalars, the same scalar name will be made active - for returned cell voxels (as active cell scalars). + The active cell or point scalars at the input will be set as active point or + cell scalars at the output, respectively. Parameters ---------- points_to_cells : bool - Set to ``True`` to convert point voxels to cell voxels. - Set to ``False`` to convert cell voxels to point voxels. + Set to ``True`` to re-mesh points to cells. + Set to ``False`` to re-mesh cells to points. scalars : str If set, only these scalars are passed through. @@ -1205,7 +1240,7 @@ def _convert_points_cells(self, points_to_cells: bool, scalars: Optional[str], c Returns ------- pyvista.ImageData - Image with converted voxels. + Re-meshed image. """ def _get_output_scalars(preference): diff --git a/tests/core/test_dataobject.py b/tests/core/test_dataobject.py index 5833c5300c..f111b21f48 100644 --- a/tests/core/test_dataobject.py +++ b/tests/core/test_dataobject.py @@ -198,13 +198,13 @@ def test_user_dict_persists_with_pack_labels_filter(): assert image.user_dict['name'] == 'image' -def test_user_dict_persists_with_point_voxels_to_cell_voxels(uniform): +def test_user_dict_persists_with_points_to_cells(uniform): uniform.user_dict['name'] = 'image' uniform.cells_to_points() assert uniform.user_dict['name'] == 'image' -def test_user_dict_persists_with_cell_voxels_to_point_voxels(uniform): +def test_user_dict_persists_with_cells_to_points(uniform): uniform.user_dict['name'] = 'image' uniform.points_to_cells() assert uniform.user_dict['name'] == 'image' From e8f05a5f2947929400de67a7f2bfd0f95a56393c Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Thu, 16 May 2024 16:30:29 -0600 Subject: [PATCH 11/19] Update docs, create standalone examples --- .pre-commit-config.yaml | 10 +- examples/01-filter/image-representations.py | 162 +++++++++++++ pyvista/core/filters/data_set.py | 3 +- pyvista/core/filters/image_data.py | 237 +++++++++----------- 4 files changed, 270 insertions(+), 142 deletions(-) create mode 100644 examples/01-filter/image-representations.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e5b23206f..e96ee89276 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -119,8 +119,8 @@ repos: - id: ruff args: [--fix] -# - repo: https://github.com/pre-commit/mirrors-prettier -# rev: v3.1.0 -# hooks: -# - id: prettier -# types_or: [yaml, markdown, html, css, scss, javascript, json] + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [yaml, markdown, html, css, scss, javascript, json] diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py new file mode 100644 index 0000000000..60a137a80e --- /dev/null +++ b/examples/01-filter/image-representations.py @@ -0,0 +1,162 @@ +""" + +.. _image_representations_example: + +Image Data Representations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example demonstrates how to use :meth:`~pyvista.ImageDataFilters.points_to_cells`. +and :meth:`~pyvista.ImageDataFilters.cells_to_points` to re-mesh :class:`~pyvista.ImageData`. + +These filters are used to ensure that image data has the appropriate representation +when generating plots and/or when using either point- or cell-based filters such +as :meth:`~pyvista.ImageDataFilters.image_threshold` (point-based) and +:meth:`~pyvista.DataSetFilters.threshold` (cell-based). + +""" + +################################################################################ +# Compare Representations +# ---------------------------------------- +# Create image data with eight points and generate discrete scalar data. + +import numpy as np + +import pyvista as pv + +points_image = pv.ImageData(dimensions=(2, 2, 2)) +points_image.point_data['Data'] = list(range(8))[::-1] +points_image.n_points +points_image.n_cells +points_image.dimensions + +################################################################################ +# If we plot the image, it is represented as a single cell with eight points, +# and the point data is interpolated to color the cell. + +points_image.plot(show_edges=True) + +################################################################################ +# However, in many applications (e.g. 3D medical images) the scalar data arrays +# represent discretized samples at the center-points of voxels. As such, it may +# be preferred to represent the data as eight cells instead of eight points. In +# this case, we can re-mesh the point data to cell data to create a cell-based +# representation. + +cells_image = points_image.points_to_cells() +cells_image.n_points +cells_image.n_cells +cells_image.dimensions + +################################################################################ +# Now, when we plot the image, we have a more appropriate representation with +# eight voxel cells, and the scalar data is no longer interpolated. + +cells_image.plot(show_edges=True) + +################################################################################ +# Show the two representations together. Note how the cell centers of the cells +# image correspond to the points of the points image. + +cell_centers = cells_image.cell_centers() +cell_edges = cells_image.extract_all_edges() + +plot = pv.Plotter() +plot.add_mesh(points_image, color=True, show_edges=True, opacity=0.7) +plot.add_mesh(cell_edges, color='black') +plot.add_points( + cell_centers, + render_points_as_spheres=True, + color='red', + point_size=20, +) +plot.camera.azimuth = -25 +plot.camera.elevation = 25 +plot.show() + +################################################################################ +# As long as only one kind of scalar data is used (i.e. either point or cell +# data, but not both), it is possible to move between representations without +# affecting the values of the scalar data. + +points_image.active_scalars +points_image.points_to_cells().cells_to_points().active_scalars + +################################################################################ +# Using Point Filters with Image Data +# ----------------------------------- +# With a point-based representation of the image, we can use a point-based +# filter such as :meth:`~pyvista.ImageDataFilters.image_threshold`. + +points_thresh = points_image.image_threshold(2) + +################################################################################ +# The filter works as expected, but when we plot it the values are interpolated +# as before. + +points_thresh.plot(show_edges=True) + +################################################################################ +# Convert the point-based output from the filter to a cell representation to +# better visualize the result. + +cells_thresh = points_thresh.points_to_cells() +cells_thresh.plot(show_edges=True) + +################################################################################ +# Using Cell Filters with Image Data +# ---------------------------------- +# With a cell-based representation of the image, we can use a cell-based filter +# such as :meth:`~pyvista.DataSetFilters.threshold`. + +cells_thresh = cells_image.threshold(2) +cells_thresh.plot(show_edges=True) + +################################################################################ +# Using the cell representation with this filter produces the expected result +# since the original scalar data represents discrete voxels. +# +# For comparison, let's apply the same filter to the point-based representation. + +points_thresh = points_image.threshold(2) +points_thresh.plot(show_edges=True) + +################################################################################ +# We can see that applying the filter to the point representation of the data +# produces a very different result than applying the same filter to the cell +# representation. In fact, the plot of the output (thresholded image) is +# identical to the plot of the input (points image) shown at the start of this +# example. Since the input points image only has a single cell, the cell-based +# filter had no effect on the data. + +points_thresh['Data'] +points_image['Data'] + +################################################################################ +# Representations for 2D Images +# ----------------------------- +# Create a 2D grayscale image with 16 points representing 16 pixels. + +gray_points = pv.ImageData(dimensions=(4, 4, 1)) +gray_points.point_data['Data'] = np.linspace(0, 255, 16, dtype=np.uint8) + +################################################################################ +# Plot the image. As before, the plot does not appear correct since the point +# data is interpolated and nine cells are shown rather than the desired 16 +# (one for each pixel). + +plot_kwargs = dict( + cpos='xy', + zoom='tight', + show_axes=False, + cmap='gray', + clim=[0, 255], + show_edges=True, +) +gray_points.plot(**plot_kwargs) + +################################################################################ +# Plot the image as cells instead to show 16 pixels with discrete values. + +gray_cells = gray_points.points_to_cells() +gray_cells.plot(**plot_kwargs) diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index 894dda1856..77e5dd791a 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -1258,7 +1258,8 @@ def threshold( ... show_edges=True, ... ) - See :ref:`common_filter_example` for more examples using this filter. + See :ref:`common_filter_example` and :ref:`image_representations_example` for + more examples using this filter. """ # set the scalars to threshold on diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index d6da042b8d..454c3f1824 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -49,10 +49,9 @@ def gaussian_smooth(self, radius_factor=1.5, std_dev=2.0, scalars=None, progress Notes ----- - This filter only supports point data. Consider converting any cell data - to point data using the :func:`cell_data_to_point_data() - ` filter to convert any - cell data to point data. + This filter only supports point data. For inputs with cell data, consider + re-meshing the cell data as point data with :meth:`~pyvista.ImageDataFilters.cells_to_points` + or resampling the cell data to point data with :func:`~pyvista.DataSetFilters.cell_data_to_point_data`. Examples -------- @@ -289,10 +288,9 @@ def image_dilate_erode( Notes ----- - This filter only supports point data. Consider converting any cell data - to point data using the :func:`cell_data_to_point_data() - ` filter to convert ny - cell data to point data. + This filter only supports point data. For inputs with cell data, consider + re-meshing the cell data as point data with :meth:`~pyvista.ImageDataFilters.cells_to_points` + or resampling the cell data to point data with :func:`~pyvista.DataSetFilters.cell_data_to_point_data`. Examples -------- @@ -392,6 +390,10 @@ def image_threshold( pyvista.ImageData Dataset with the specified scalars thresholded. + See Also + -------- + :meth:`~pyvista.DataSetFilters.threshold` + Examples -------- Demonstrate image threshold on an example dataset. First, plot @@ -407,6 +409,8 @@ def image_threshold( >>> ithresh = uni.image_threshold(100) >>> ithresh.plot() + See :ref:`image_representations_example` for more examples using this filter. + """ alg = _vtk.vtkImageThreshold() alg.SetInputDataObject(self) @@ -952,7 +956,7 @@ def contour_labeled( return cast(pyvista.PolyData, wrap(alg.GetOutput())) def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): - """Re-mesh image data from a points-based to a cells-based representation. + """Re-mesh image data from a point-based to a cell-based representation. This filter changes how image data is represented. Data represented as points at the input is re-meshed into an alternative representation as cells at the @@ -1002,87 +1006,59 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): Returns ------- pyvista.ImageData - Image with a cells-based representation. + Image with a cell-based representation. Examples -------- - Create image data with eight points. + Load an image with point data. - >>> import pyvista as pv - >>> points_image = pv.ImageData(dimensions=(2, 2, 2)) - >>> points_image.point_data['Data'] = list(range(8))[::-1] - >>> points_image.n_points - 8 - >>> points_image.n_cells - 1 - >>> points_image.dimensions - (2, 2, 2) - - If we plot the image, it is represented as a single cell with eight points. The - point data is interpolated to color the cell. - - >>> points_image.plot(show_edges=True) - - However, in many applications (e.g. 3D medical images) the point samples are - used to represent the center-points of voxels with discrete values. As such, it - may be preferred to represent the data as eight cells instead of eight points. In - this case, we can re-mesh the point data to cell data to create a cell-based - representation. - - >>> cells_image = points_image.points_to_cells() - >>> cells_image.n_points - 27 - >>> cells_image.n_cells - 8 - >>> cells_image.dimensions - (3, 3, 3) - - Now, when we plot the image, we have a more appropriate representation with eight - voxel cells, and the discrete data is no longer interpolated. - - >>> cells_image.plot(show_edges=True) - - Show the two representations together. The cell centers of the cells image - correspond to the points of the points image. - - >>> cell_centers = cells_image.cell_centers() - >>> cell_edges = cells_image.extract_all_edges() - >>> - >>> plot = pv.Plotter() - >>> _ = plot.add_mesh( - ... points_image, color=True, show_edges=True, opacity=0.7 - ... ) - >>> _ = plot.add_mesh(cell_edges, color='black') - >>> _ = plot.add_points( - ... cell_centers, - ... render_points_as_spheres=True, - ... color='red', - ... point_size=20, - ... ) - >>> _ = plot.camera.azimuth = -25 - >>> _ = plot.camera.elevation = 25 - >>> plot.show() + >>> from pyvista import examples + >>> image = examples.load_uniform() + + Show the current properties and point arrays of the image. + + >>> image + ImageData (...) + N Cells: 729 + N Points: 1000 + X Bounds: 0.000e+00, 9.000e+00 + Y Bounds: 0.000e+00, 9.000e+00 + Z Bounds: 0.000e+00, 9.000e+00 + Dimensions: 10, 10, 10 + Spacing: 1.000e+00, 1.000e+00, 1.000e+00 + N Arrays: 2 + + >>> image.point_data.keys() + ['Spatial Point Data'] - With the data represented as cells, we can now use a cell-based filter such as - :meth:`~pyvista.DataSetFilters.threshold`. Since the original image data - represents discrete voxels, this filter produces the expected result. + Re-mesh the points and point data as cells and cell data. - >>> cells_thresh = cells_image.threshold(2) - >>> cells_thresh.plot(show_edges=True) + >>> cells_image = image.points_to_cells() - For comparison, apply the same filter to the points-based representation. + Show the properties and cell arrays of the re-meshed image. - >>> points_thresh = points_image.threshold(2) - >>> points_thresh.plot(show_edges=True) + >>> cells_image + ImageData (...) + N Cells: 1000 + N Points: 1331 + X Bounds: -5.000e-01, 9.500e+00 + Y Bounds: -5.000e-01, 9.500e+00 + Z Bounds: -5.000e-01, 9.500e+00 + Dimensions: 11, 11, 11 + Spacing: 1.000e+00, 1.000e+00, 1.000e+00 + N Arrays: 1 - We can see that this produces a very different result. In fact, since the input - only has a single cell, using the cell-based filter with the points-based - representation had no effect on the scalar data. + >>> cells_image.cell_data.keys() + ['Spatial Point Data'] - >>> points_thresh['Data'] - pyvista_ndarray([7, 6, 5, 4, 3, 2, 1, 0]) - >>> points_image['Data'] - pyvista_ndarray([7, 6, 5, 4, 3, 2, 1, 0]) + Observe that: + - The input point array is now a cell array + - The output has one less array (the input cell data is ignored) + - The dimensions have increased by one + - The bounds have increased by half the spacing + - The output N Cells equals the input N Points + + See :ref:`image_representations_example` for a full example using this filter. """ if scalars is not None: @@ -1094,7 +1070,7 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): return self._remesh_points_cells(points_to_cells=True, scalars=scalars, copy=copy) def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): - """Re-mesh image data from a cells-based to a points-based representation. + """Re-mesh image data from a cell-based to a point-based representation. This filter changes how image data is represented. Data represented as cells at the input is re-meshed into an alternative representation as points at the @@ -1144,71 +1120,59 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): Returns ------- pyvista.ImageData - Image with a points-based representation. + Image with a point-based representation. Examples -------- - Create image data with eight cells. + Load an image with cell data. - >>> import pyvista as pv - >>> cells_image = pv.ImageData(dimensions=(3, 3, 3)) - >>> cells_image.cell_data['Data'] = list(range(8))[::-1] - >>> cells_image.n_points - 27 - >>> cells_image.n_cells - 8 - >>> cells_image.dimensions - (3, 3, 3) - - If we plot the image, it is represented as eight cells with 27 points. - - >>> cells_image.plot(show_edges=True) - - Alternatively, we can represent the cells as eight points instead. - - >>> points_image = cells_image.cells_to_points() - >>> points_image.n_points - 8 - >>> points_image.n_cells - 1 - >>> points_image.dimensions - (2, 2, 2) - >>> points_image.plot(show_edges=True) - - Show the two representations together. The points of the points image correspond - to the cell centers of the cells image. - - >>> cell_centers = cells_image.cell_centers() - >>> cell_edges = cells_image.extract_all_edges() - >>> - >>> plot = pv.Plotter() - >>> _ = plot.add_mesh( - ... points_image, color=True, show_edges=True, opacity=0.7 - ... ) - >>> _ = plot.add_mesh(cell_edges, color='black') - >>> _ = plot.add_points( - ... cell_centers, - ... render_points_as_spheres=True, - ... color='red', - ... point_size=20, - ... ) - >>> _ = plot.camera.azimuth = -25 - >>> _ = plot.camera.elevation = 25 - >>> plot.show() + >>> from pyvista import examples + >>> image = examples.load_uniform() + + Show the current properties and cell arrays of the image. + + >>> image + ImageData (...) + N Cells: 729 + N Points: 1000 + X Bounds: 0.000e+00, 9.000e+00 + Y Bounds: 0.000e+00, 9.000e+00 + Z Bounds: 0.000e+00, 9.000e+00 + Dimensions: 10, 10, 10 + Spacing: 1.000e+00, 1.000e+00, 1.000e+00 + N Arrays: 2 - With a points-based representation, we can now use a points-based filter with - our dataset such as :meth:`~pyvista.ImageDataFilters.image_threshold`. + >>> image.cell_data.keys() + ['Spatial Cell Data'] - >>> points_thresh = points_image.image_threshold(2) + Re-mesh the cells and cell data as points and point data. - The filter works as expected, but when we plot it the values are interpolated. + >>> points_image = image.cells_to_points() - >>> points_thresh.plot(show_edges=True) + Show the properties and point arrays of the re-meshed image. - Convert the result back to a cell representation to better visualize the result. + >>> points_image + ImageData (...) + N Cells: 512 + N Points: 729 + X Bounds: 5.000e-01, 8.500e+00 + Y Bounds: 5.000e-01, 8.500e+00 + Z Bounds: 5.000e-01, 8.500e+00 + Dimensions: 9, 9, 9 + Spacing: 1.000e+00, 1.000e+00, 1.000e+00 + N Arrays: 1 - >>> cells_thresh = points_thresh.points_to_cells() - >>> cells_thresh.plot(show_edges=True) + >>> points_image.point_data.keys() + ['Spatial Cell Data'] + + Observe that: + - The input cell array is now a point array + - The output has one less array (the input point data is ignored) + - The dimensions have decreased by one + - The bounds have decreased by half the spacing + - The output N Points equals the input N Cells + + See :ref:`image_representations_example` for a full example using this filter. """ if scalars is not None: @@ -1241,6 +1205,7 @@ def _remesh_points_cells(self, points_to_cells: bool, scalars: Optional[str], co ------- pyvista.ImageData Re-meshed image. + """ def _get_output_scalars(preference): From 5c09fc4fab834ea58f249f9c14f6f4d0e1c6dafc Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Thu, 16 May 2024 16:49:38 -0600 Subject: [PATCH 12/19] Fix remeshing 2D images --- pyvista/core/filters/image_data.py | 4 ++-- tests/core/test_imagedata_filters.py | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 454c3f1824..06e1164298 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -1243,8 +1243,8 @@ def _get_output_scalars(preference): np.array(self.spacing) / 2, # type: ignore[attr-defined] ) new_image.dimensions = dims_operator( - np.array(self.dimensions), # type: ignore[attr-defined] - 1, + dims := np.array(self.dimensions), # type: ignore[attr-defined] + dims > 1, # Only operate on non-singleton dimensions ) new_image.spacing = self.spacing # type: ignore[attr-defined] new_image.SetDirectionMatrix(self.GetDirectionMatrix()) # type: ignore[attr-defined] diff --git a/tests/core/test_imagedata_filters.py b/tests/core/test_imagedata_filters.py index 0bbc58ae49..cf53e7d704 100644 --- a/tests/core/test_imagedata_filters.py +++ b/tests/core/test_imagedata_filters.py @@ -9,6 +9,11 @@ VTK93 = pv.vtk_version_info >= (9, 3) +@pytest.fixture() +def logo(): + return examples.load_logo() + + @pytest.mark.skipif(not VTK93, reason='At least VTK 9.3 is required') def test_contour_labeled(): # Load a 3D label map (segmentation of a frog's tissue) @@ -218,6 +223,16 @@ def test_cells_to_points_scalars(uniform): uniform.cells_to_points('Spatial Point Data') +def test_points_to_cells_and_cells_to_points_dimensions(uniform, logo): + assert uniform.dimensions == (10, 10, 10) + assert uniform.points_to_cells().dimensions == (11, 11, 11) + assert uniform.cells_to_points().dimensions == (9, 9, 9) + + assert logo.dimensions == (1920, 718, 1) + assert logo.points_to_cells().dimensions == (1921, 719, 1) + assert logo.cells_to_points().dimensions == (1919, 717, 1) + + @pytest.fixture() def single_point_image(): image = pv.ImageData(dimensions=(1, 1, 1)) @@ -373,11 +388,6 @@ def test_pad_image_wrap_mirror(uniform, pad_value): assert np.array_equal(padded_scalars3D[1:-1, 0, 0], scalars3D[:, 0, 0]) -@pytest.fixture() -def logo(): - return examples.load_logo() - - def test_pad_image_multi_component(single_point_image): single_point_image.clear_data() new_value = np.array([10, 20, 30, 40]) From 6861f4c3854936dab3b1189708658477b70bed51 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Thu, 16 May 2024 22:39:54 -0600 Subject: [PATCH 13/19] Update examples --- examples/01-filter/image-representations.py | 139 ++++++++++++-------- pyvista/core/filters/image_data.py | 6 +- 2 files changed, 91 insertions(+), 54 deletions(-) diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py index 60a137a80e..668b353901 100644 --- a/examples/01-filter/image-representations.py +++ b/examples/01-filter/image-representations.py @@ -5,30 +5,32 @@ Image Data Representations ~~~~~~~~~~~~~~~~~~~~~~~~~~ -This example demonstrates how to use :meth:`~pyvista.ImageDataFilters.points_to_cells`. +This example demonstrates how to use :meth:`~pyvista.ImageDataFilters.points_to_cells` and :meth:`~pyvista.ImageDataFilters.cells_to_points` to re-mesh :class:`~pyvista.ImageData`. -These filters are used to ensure that image data has the appropriate representation -when generating plots and/or when using either point- or cell-based filters such -as :meth:`~pyvista.ImageDataFilters.image_threshold` (point-based) and -:meth:`~pyvista.DataSetFilters.threshold` (cell-based). +These filters can be used to ensure that image data has an appropriate representation +when generating plots and/or when using either point- or cell-based filters such as +:meth:`ImageDataFilters.image_threshold ` (point-based) + and :meth:`DataSetFilters.threshold ` (cell-based). """ ################################################################################ # Compare Representations -# ---------------------------------------- -# Create image data with eight points and generate discrete scalar data. +# ----------------------- +# Create image data with eight points and a discrete scalar data array. import numpy as np import pyvista as pv +data_array = [8, 7, 6, 5, 4, 3, 1, 0] points_image = pv.ImageData(dimensions=(2, 2, 2)) -points_image.point_data['Data'] = list(range(8))[::-1] +points_image.point_data['Data'] = data_array + points_image.n_points + points_image.n_cells -points_image.dimensions ################################################################################ # If we plot the image, it is represented as a single cell with eight points, @@ -39,14 +41,11 @@ ################################################################################ # However, in many applications (e.g. 3D medical images) the scalar data arrays # represent discretized samples at the center-points of voxels. As such, it may -# be preferred to represent the data as eight cells instead of eight points. In -# this case, we can re-mesh the point data to cell data to create a cell-based -# representation. +# be more appropriate to represent the data as eight cells instead of eight +# points. We can use :meth:`~pyvista.ImageDataFilters.points_to_cells` to +# generate a cell-based representation. cells_image = points_image.points_to_cells() -cells_image.n_points -cells_image.n_cells -cells_image.dimensions ################################################################################ # Now, when we plot the image, we have a more appropriate representation with @@ -55,8 +54,11 @@ cells_image.plot(show_edges=True) ################################################################################ -# Show the two representations together. Note how the cell centers of the cells -# image correspond to the points of the points image. +# Let's compare the two representations and plot them together. +# +# For visualization, we color the points image (inner mesh) and show the cells +# image (outer mesh) as a wireframe. We also plot the cell centers in red. Note +# how the centers of the cells image correspond to the points of the points image. cell_centers = cells_image.cell_centers() cell_edges = cells_image.extract_all_edges() @@ -77,68 +79,99 @@ ################################################################################ # As long as only one kind of scalar data is used (i.e. either point or cell # data, but not both), it is possible to move between representations without -# affecting the values of the scalar data. +# loss of data. points_image.active_scalars + points_image.points_to_cells().cells_to_points().active_scalars ################################################################################ -# Using Point Filters with Image Data -# ----------------------------------- -# With a point-based representation of the image, we can use a point-based -# filter such as :meth:`~pyvista.ImageDataFilters.image_threshold`. +# Point Filters with Image Data +# ----------------------------- +# Use a point representation of the image when working with point-based +# filters such as :meth:`~pyvista.ImageDataFilters.image_threshold`. If the +# image only has cell data, use :meth:`~pyvista.ImageDataFilters.cells_to_points` +# re-mesh the input first. +# +# Here, we reuse the points image defined earlier and apply the filter. For +# context, we also show the input data array. + +points_image.point_data['Data'] -points_thresh = points_image.image_threshold(2) +points_ithresh = points_image.image_threshold(2) ################################################################################ -# The filter works as expected, but when we plot it the values are interpolated -# as before. +# This filter returns binary point data as expected. Values above the threshold +# of ``2`` are ones, and below the threshold are zeros. -points_thresh.plot(show_edges=True) +points_ithresh.point_data['Data'] ################################################################################ -# Convert the point-based output from the filter to a cell representation to -# better visualize the result. +# However, when we plot it the point values are interpolated as before. -cells_thresh = points_thresh.points_to_cells() -cells_thresh.plot(show_edges=True) +points_ithresh.plot(show_edges=True) + +################################################################################ +# To better visualize the result, convert the points image returned by the +# filter to a cell representation with :meth:`~pyvista.ImageDataFilters.points_to_cells` +# before plotting. + +points_ithresh_as_cells = points_ithresh.points_to_cells() +points_ithresh_as_cells.plot(show_edges=True) ################################################################################ -# Using Cell Filters with Image Data -# ---------------------------------- -# With a cell-based representation of the image, we can use a cell-based filter -# such as :meth:`~pyvista.DataSetFilters.threshold`. +# Cell Filters with Image Data +# ---------------------------- +# Use a cell representation of the image when working with cell-based filters +# such as :meth:`~pyvista.DataSetFilters.threshold`. If the image only has point +# data, use :meth:`~pyvista.ImageDataFilters.points_to_cells` to re-mesh the +# input first. +# +# Here, we reuse the cells image created earlier and apply the filter. For +# context, we also show the input data array. + +cells_image.cell_data['Data'] cells_thresh = cells_image.threshold(2) + +################################################################################ +# When the input is cell data, this filter returns six discrete values above +# the threshold value of ``2`` as expected. + +cells_thresh.cell_data['Data'] + cells_thresh.plot(show_edges=True) ################################################################################ -# Using the cell representation with this filter produces the expected result -# since the original scalar data represents discrete voxels. -# -# For comparison, let's apply the same filter to the point-based representation. +# However, if we apply the same filter to a point-based representation of the +# image, the filter returns an unexpected result. points_thresh = points_image.threshold(2) -points_thresh.plot(show_edges=True) ################################################################################ -# We can see that applying the filter to the point representation of the data -# produces a very different result than applying the same filter to the cell -# representation. In fact, the plot of the output (thresholded image) is -# identical to the plot of the input (points image) shown at the start of this -# example. Since the input points image only has a single cell, the cell-based -# filter had no effect on the data. +# In this case, the filter has no effect on the data array's values. -points_thresh['Data'] -points_image['Data'] +points_thresh.point_data['Data'] ################################################################################ -# Representations for 2D Images -# ----------------------------- -# Create a 2D grayscale image with 16 points representing 16 pixels. +# If we plot the output, the result is identical to the plot of the input points +# image shown at the beginning of this example. + +points_thresh.plot(show_edges=True) + +################################################################################ +# Representations of 2D Images +# ---------------------------- +# The filters :meth:`~pyvista.ImageDataFilters.points_to_cells` and +# :meth:`~pyvista.ImageDataFilters.cells_to_points` can similarly be used +# with 2D images. +# +# For this example, we create a 4x4 2D grayscale image with 16 points to represent +# 16 pixels. +data_array = np.linspace(0, 255, 16, dtype=np.uint8)[::-1] gray_points = pv.ImageData(dimensions=(4, 4, 1)) -gray_points.point_data['Data'] = np.linspace(0, 255, 16, dtype=np.uint8) +gray_points.point_data['Data'] = data_array ################################################################################ # Plot the image. As before, the plot does not appear correct since the point @@ -156,7 +189,9 @@ gray_points.plot(**plot_kwargs) ################################################################################ -# Plot the image as cells instead to show 16 pixels with discrete values. +# To visualize the image correctly, we first use :meth:`~pyvista.ImageDataFilters.points_to_cells` +# to get a cell-based representation of the image and plot the result. The plot +# now correctly shows 16 pixel cells with discrete values. gray_cells = gray_points.points_to_cells() gray_cells.plot(**plot_kwargs) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index 06e1164298..c83b47ff66 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -1052,13 +1052,14 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): ['Spatial Point Data'] Observe that: + - The input point array is now a cell array - The output has one less array (the input cell data is ignored) - The dimensions have increased by one - The bounds have increased by half the spacing - The output N Cells equals the input N Points - See :ref:`image_representations_example` for a full example using this filter. + See :ref:`image_representations_example` for more examples using this filter. """ if scalars is not None: @@ -1166,13 +1167,14 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): ['Spatial Cell Data'] Observe that: + - The input cell array is now a point array - The output has one less array (the input point data is ignored) - The dimensions have decreased by one - The bounds have decreased by half the spacing - The output N Points equals the input N Cells - See :ref:`image_representations_example` for a full example using this filter. + See :ref:`image_representations_example` for more examples using this filter. """ if scalars is not None: From cbb53f722250f0e1f86ff7917b1b62954bcc23c5 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 07:49:29 -0600 Subject: [PATCH 14/19] Add print statements --- examples/01-filter/image-representations.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py index 668b353901..79c9b5b99a 100644 --- a/examples/01-filter/image-representations.py +++ b/examples/01-filter/image-representations.py @@ -28,9 +28,9 @@ points_image = pv.ImageData(dimensions=(2, 2, 2)) points_image.point_data['Data'] = data_array -points_image.n_points +print(points_image.n_points) -points_image.n_cells +print(points_image.n_cells) ################################################################################ # If we plot the image, it is represented as a single cell with eight points, @@ -81,9 +81,9 @@ # data, but not both), it is possible to move between representations without # loss of data. -points_image.active_scalars +print(points_image.active_scalars) -points_image.points_to_cells().cells_to_points().active_scalars +print(points_image.points_to_cells().cells_to_points().active_scalars) ################################################################################ # Point Filters with Image Data @@ -96,7 +96,7 @@ # Here, we reuse the points image defined earlier and apply the filter. For # context, we also show the input data array. -points_image.point_data['Data'] +print(points_image.point_data['Data']) points_ithresh = points_image.image_threshold(2) @@ -104,7 +104,7 @@ # This filter returns binary point data as expected. Values above the threshold # of ``2`` are ones, and below the threshold are zeros. -points_ithresh.point_data['Data'] +print(points_ithresh.point_data['Data']) ################################################################################ # However, when we plot it the point values are interpolated as before. @@ -130,7 +130,7 @@ # Here, we reuse the cells image created earlier and apply the filter. For # context, we also show the input data array. -cells_image.cell_data['Data'] +print(cells_image.cell_data['Data']) cells_thresh = cells_image.threshold(2) @@ -138,7 +138,7 @@ # When the input is cell data, this filter returns six discrete values above # the threshold value of ``2`` as expected. -cells_thresh.cell_data['Data'] +print(cells_thresh.cell_data['Data']) cells_thresh.plot(show_edges=True) @@ -151,7 +151,7 @@ ################################################################################ # In this case, the filter has no effect on the data array's values. -points_thresh.point_data['Data'] +print(points_thresh.point_data['Data']) ################################################################################ # If we plot the output, the result is identical to the plot of the input points From 8e3ce4529fa26c9ff3f52f6bd6b3d8459ff66dbf Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 09:15:26 -0600 Subject: [PATCH 15/19] Update printing variables --- examples/01-filter/image-representations.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py index 79c9b5b99a..40cd84f751 100644 --- a/examples/01-filter/image-representations.py +++ b/examples/01-filter/image-representations.py @@ -28,10 +28,6 @@ points_image = pv.ImageData(dimensions=(2, 2, 2)) points_image.point_data['Data'] = data_array -print(points_image.n_points) - -print(points_image.n_cells) - ################################################################################ # If we plot the image, it is represented as a single cell with eight points, # and the point data is interpolated to color the cell. @@ -81,9 +77,9 @@ # data, but not both), it is possible to move between representations without # loss of data. -print(points_image.active_scalars) - -print(points_image.points_to_cells().cells_to_points().active_scalars) +array_before = points_image.active_scalars +array_after = points_image.points_to_cells().cells_to_points().active_scalars +np.array_equal(array_before, array_after) ################################################################################ # Point Filters with Image Data @@ -96,7 +92,7 @@ # Here, we reuse the points image defined earlier and apply the filter. For # context, we also show the input data array. -print(points_image.point_data['Data']) +points_image.point_data['Data'] points_ithresh = points_image.image_threshold(2) @@ -104,7 +100,7 @@ # This filter returns binary point data as expected. Values above the threshold # of ``2`` are ones, and below the threshold are zeros. -print(points_ithresh.point_data['Data']) +points_ithresh.point_data['Data'] ################################################################################ # However, when we plot it the point values are interpolated as before. @@ -130,7 +126,7 @@ # Here, we reuse the cells image created earlier and apply the filter. For # context, we also show the input data array. -print(cells_image.cell_data['Data']) +cells_image.cell_data['Data'] cells_thresh = cells_image.threshold(2) @@ -138,7 +134,7 @@ # When the input is cell data, this filter returns six discrete values above # the threshold value of ``2`` as expected. -print(cells_thresh.cell_data['Data']) +cells_thresh.cell_data['Data'] cells_thresh.plot(show_edges=True) @@ -151,7 +147,7 @@ ################################################################################ # In this case, the filter has no effect on the data array's values. -print(points_thresh.point_data['Data']) +points_thresh.point_data['Data'] ################################################################################ # If we plot the output, the result is identical to the plot of the input points From 8c5dad8e967ccd9b5589993ae2c7238964b2d66d Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 13:52:46 -0600 Subject: [PATCH 16/19] Fix spacing --- examples/01-filter/image-representations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py index 40cd84f751..578c1a41b5 100644 --- a/examples/01-filter/image-representations.py +++ b/examples/01-filter/image-representations.py @@ -1,17 +1,15 @@ """ - .. _image_representations_example: Image Data Representations ~~~~~~~~~~~~~~~~~~~~~~~~~~ - This example demonstrates how to use :meth:`~pyvista.ImageDataFilters.points_to_cells` and :meth:`~pyvista.ImageDataFilters.cells_to_points` to re-mesh :class:`~pyvista.ImageData`. These filters can be used to ensure that image data has an appropriate representation when generating plots and/or when using either point- or cell-based filters such as :meth:`ImageDataFilters.image_threshold ` (point-based) - and :meth:`DataSetFilters.threshold ` (cell-based). +and :meth:`DataSetFilters.threshold ` (cell-based). """ From 593c6764d3f4de18461454c848743e072508b3c3 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 19:16:21 -0600 Subject: [PATCH 17/19] Remove example file --- examples/01-filter/image-representations.py | 191 -------------------- 1 file changed, 191 deletions(-) delete mode 100644 examples/01-filter/image-representations.py diff --git a/examples/01-filter/image-representations.py b/examples/01-filter/image-representations.py deleted file mode 100644 index 578c1a41b5..0000000000 --- a/examples/01-filter/image-representations.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -.. _image_representations_example: - -Image Data Representations -~~~~~~~~~~~~~~~~~~~~~~~~~~ -This example demonstrates how to use :meth:`~pyvista.ImageDataFilters.points_to_cells` -and :meth:`~pyvista.ImageDataFilters.cells_to_points` to re-mesh :class:`~pyvista.ImageData`. - -These filters can be used to ensure that image data has an appropriate representation -when generating plots and/or when using either point- or cell-based filters such as -:meth:`ImageDataFilters.image_threshold ` (point-based) -and :meth:`DataSetFilters.threshold ` (cell-based). - -""" - -################################################################################ -# Compare Representations -# ----------------------- -# Create image data with eight points and a discrete scalar data array. - -import numpy as np - -import pyvista as pv - -data_array = [8, 7, 6, 5, 4, 3, 1, 0] -points_image = pv.ImageData(dimensions=(2, 2, 2)) -points_image.point_data['Data'] = data_array - -################################################################################ -# If we plot the image, it is represented as a single cell with eight points, -# and the point data is interpolated to color the cell. - -points_image.plot(show_edges=True) - -################################################################################ -# However, in many applications (e.g. 3D medical images) the scalar data arrays -# represent discretized samples at the center-points of voxels. As such, it may -# be more appropriate to represent the data as eight cells instead of eight -# points. We can use :meth:`~pyvista.ImageDataFilters.points_to_cells` to -# generate a cell-based representation. - -cells_image = points_image.points_to_cells() - -################################################################################ -# Now, when we plot the image, we have a more appropriate representation with -# eight voxel cells, and the scalar data is no longer interpolated. - -cells_image.plot(show_edges=True) - -################################################################################ -# Let's compare the two representations and plot them together. -# -# For visualization, we color the points image (inner mesh) and show the cells -# image (outer mesh) as a wireframe. We also plot the cell centers in red. Note -# how the centers of the cells image correspond to the points of the points image. - -cell_centers = cells_image.cell_centers() -cell_edges = cells_image.extract_all_edges() - -plot = pv.Plotter() -plot.add_mesh(points_image, color=True, show_edges=True, opacity=0.7) -plot.add_mesh(cell_edges, color='black') -plot.add_points( - cell_centers, - render_points_as_spheres=True, - color='red', - point_size=20, -) -plot.camera.azimuth = -25 -plot.camera.elevation = 25 -plot.show() - -################################################################################ -# As long as only one kind of scalar data is used (i.e. either point or cell -# data, but not both), it is possible to move between representations without -# loss of data. - -array_before = points_image.active_scalars -array_after = points_image.points_to_cells().cells_to_points().active_scalars -np.array_equal(array_before, array_after) - -################################################################################ -# Point Filters with Image Data -# ----------------------------- -# Use a point representation of the image when working with point-based -# filters such as :meth:`~pyvista.ImageDataFilters.image_threshold`. If the -# image only has cell data, use :meth:`~pyvista.ImageDataFilters.cells_to_points` -# re-mesh the input first. -# -# Here, we reuse the points image defined earlier and apply the filter. For -# context, we also show the input data array. - -points_image.point_data['Data'] - -points_ithresh = points_image.image_threshold(2) - -################################################################################ -# This filter returns binary point data as expected. Values above the threshold -# of ``2`` are ones, and below the threshold are zeros. - -points_ithresh.point_data['Data'] - -################################################################################ -# However, when we plot it the point values are interpolated as before. - -points_ithresh.plot(show_edges=True) - -################################################################################ -# To better visualize the result, convert the points image returned by the -# filter to a cell representation with :meth:`~pyvista.ImageDataFilters.points_to_cells` -# before plotting. - -points_ithresh_as_cells = points_ithresh.points_to_cells() -points_ithresh_as_cells.plot(show_edges=True) - -################################################################################ -# Cell Filters with Image Data -# ---------------------------- -# Use a cell representation of the image when working with cell-based filters -# such as :meth:`~pyvista.DataSetFilters.threshold`. If the image only has point -# data, use :meth:`~pyvista.ImageDataFilters.points_to_cells` to re-mesh the -# input first. -# -# Here, we reuse the cells image created earlier and apply the filter. For -# context, we also show the input data array. - -cells_image.cell_data['Data'] - -cells_thresh = cells_image.threshold(2) - -################################################################################ -# When the input is cell data, this filter returns six discrete values above -# the threshold value of ``2`` as expected. - -cells_thresh.cell_data['Data'] - -cells_thresh.plot(show_edges=True) - -################################################################################ -# However, if we apply the same filter to a point-based representation of the -# image, the filter returns an unexpected result. - -points_thresh = points_image.threshold(2) - -################################################################################ -# In this case, the filter has no effect on the data array's values. - -points_thresh.point_data['Data'] - -################################################################################ -# If we plot the output, the result is identical to the plot of the input points -# image shown at the beginning of this example. - -points_thresh.plot(show_edges=True) - -################################################################################ -# Representations of 2D Images -# ---------------------------- -# The filters :meth:`~pyvista.ImageDataFilters.points_to_cells` and -# :meth:`~pyvista.ImageDataFilters.cells_to_points` can similarly be used -# with 2D images. -# -# For this example, we create a 4x4 2D grayscale image with 16 points to represent -# 16 pixels. - -data_array = np.linspace(0, 255, 16, dtype=np.uint8)[::-1] -gray_points = pv.ImageData(dimensions=(4, 4, 1)) -gray_points.point_data['Data'] = data_array - -################################################################################ -# Plot the image. As before, the plot does not appear correct since the point -# data is interpolated and nine cells are shown rather than the desired 16 -# (one for each pixel). - -plot_kwargs = dict( - cpos='xy', - zoom='tight', - show_axes=False, - cmap='gray', - clim=[0, 255], - show_edges=True, -) -gray_points.plot(**plot_kwargs) - -################################################################################ -# To visualize the image correctly, we first use :meth:`~pyvista.ImageDataFilters.points_to_cells` -# to get a cell-based representation of the image and plot the result. The plot -# now correctly shows 16 pixel cells with discrete values. - -gray_cells = gray_points.points_to_cells() -gray_cells.plot(**plot_kwargs) From 075e3a2ba8b476f51e4b931e6e89ab5c7ce7526e Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 20:07:07 -0600 Subject: [PATCH 18/19] Remove refs to example file --- pyvista/core/filters/image_data.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyvista/core/filters/image_data.py b/pyvista/core/filters/image_data.py index c83b47ff66..f5eef34d91 100644 --- a/pyvista/core/filters/image_data.py +++ b/pyvista/core/filters/image_data.py @@ -409,8 +409,6 @@ def image_threshold( >>> ithresh = uni.image_threshold(100) >>> ithresh.plot() - See :ref:`image_representations_example` for more examples using this filter. - """ alg = _vtk.vtkImageThreshold() alg.SetInputDataObject(self) @@ -1059,8 +1057,6 @@ def points_to_cells(self, scalars: Optional[str] = None, *, copy: bool = True): - The bounds have increased by half the spacing - The output N Cells equals the input N Points - See :ref:`image_representations_example` for more examples using this filter. - """ if scalars is not None: field = self.get_array_association(scalars, preference='point') # type: ignore[attr-defined] @@ -1174,8 +1170,6 @@ def cells_to_points(self, scalars: Optional[str] = None, *, copy: bool = True): - The bounds have decreased by half the spacing - The output N Points equals the input N Cells - See :ref:`image_representations_example` for more examples using this filter. - """ if scalars is not None: field = self.get_array_association(scalars, preference='cell') # type: ignore[attr-defined] From 9ab948e2ecfc2610f2d34df2c4f6326f31f45269 Mon Sep 17 00:00:00 2001 From: user27182 <89109579+user27182@users.noreply.github.com> Date: Fri, 17 May 2024 20:08:10 -0600 Subject: [PATCH 19/19] Remove refs to example file --- pyvista/core/filters/data_set.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index 77e5dd791a..894dda1856 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -1258,8 +1258,7 @@ def threshold( ... show_edges=True, ... ) - See :ref:`common_filter_example` and :ref:`image_representations_example` for - more examples using this filter. + See :ref:`common_filter_example` for more examples using this filter. """ # set the scalars to threshold on