Repository: pyvista/pyvista

Issue:
Error in write_frame method with latest version of PyVista and ImageIO
### Describe the bug, what's wrong, and what you expected.

Updating to lastest version of Image IO  to latest version (2.28.1) I get the error attaached!
![image](https://user-images.githubusercontent.com/77293250/235624453-f7ef3449-b59a-41df-a2ea-d9f8794e93c2.png)
Reverting to 2.28.0 it works again

### Steps to reproduce the bug.

```python
pv = pv.Plotter(notebook=self.is_notebook, off_screen=True, window_size=self.windows_size)
add a mesh here
pv.write_frame()

```

### System Information

```shell
.
```


### Screenshots

_No response_

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyvista/pyvista/pull/4375?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) Report
> Merging [#4375](https://app.codecov.io/gh/pyvista/pyvista/pull/4375?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (e5d63b5) into [main](https://app.codecov.io/gh/pyvista/pyvista/commit/4c2d1aed10b1600d520271beba8579c71433e808?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyvista) (4c2d1ae) will **decrease** coverage by `2.13%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #4375      +/-   ##
==========================================
- Coverage   95.81%   93.68%   -2.13%     
==========================================
  Files          97       97              
  Lines       20814    20820       +6     
==========================================
- Hits        19942    19505     -437     
- Misses        872     1315     +443     
```


Patch:
diff --git a/pyvista/core/filters/unstructured_grid.py b/pyvista/core/filters/unstructured_grid.py
index 6a6bb99b..849bc82d 100644
--- a/pyvista/core/filters/unstructured_grid.py
+++ b/pyvista/core/filters/unstructured_grid.py
@@ -2,6 +2,7 @@
 from functools import wraps
 
 from pyvista import _vtk, abstract_class
+from pyvista.core.errors import VTKVersionError
 from pyvista.core.filters import _get_output, _update_alg
 from pyvista.core.filters.data_set import DataSetFilters
 from pyvista.core.filters.poly_data import PolyDataFilters
@@ -47,3 +48,104 @@ class UnstructuredGridFilters(DataSetFilters):
         alg.SetInputData(self)
         _update_alg(alg)
         return _get_output(alg)
+
+    def clean(
+        self,
+        tolerance=0,
+        remove_unused_points=True,
+        produce_merge_map=True,
+        average_point_data=True,
+        merging_array_name=None,
+        progress_bar=False,
+    ):
+        """Merge duplicate points and remove unused points in an UnstructuredGrid.
+
+        This filter, merging coincident points as defined by a merging
+        tolerance and optionally removes unused points. The filter does not
+        modify the topology of the input dataset, nor change the types of
+        cells. It may however, renumber the cell connectivity ids.
+
+        This filter implements `vtkStaticCleanUnstructuredGrid
+        <https://vtk.org/doc/nightly/html/classvtkStaticCleanUnstructuredGrid.html>`_
+
+        Parameters
+        ----------
+        tolerance : float, default: 0.0
+            The absolute point merging tolerance.
+
+        remove_unused_points : bool, default: True
+            Indicate whether points unused by any cell are removed from the
+            output. Note that when this is off, the filter can successfully
+            process datasets with no cells (and just points). If on in this
+            case, and there are no cells, the output will be empty.
+
+        produce_merge_map : bool, default: False
+            Indicate whether a merge map should be produced on output.
+            The merge map, if requested, maps each input point to its
+            output point id, or provides a value of -1 if the input point
+            is not used in the output. The merge map is associated with
+            the filter's output field data and is named ``"PointMergeMap"``.
+
+        average_point_data : bool, default: True
+            Indicate whether point coordinates and point data of merged points
+            are averaged. When ``True``, the data coordinates and attribute
+            values of all merged points are averaged. When ``False``, the point
+            coordinate and data of the single remaining merged point is
+            retained.
+
+        merging_array_name : str, optional
+            If a ``merging_array_name`` is specified and exists in the
+            ``point_data``, then point merging will switch into a mode where
+            merged points must be both geometrically coincident and have
+            matching point data. When set, ``tolerance`` has no effect.
+
+        progress_bar : bool, default: False
+            Display a progress bar to indicate progress.
+
+        Returns
+        -------
+        UnstructuredGrid
+            Cleaned unstructured grid.
+
+        Examples
+        --------
+        Demonstrate cleaning an UnstructuredGrid and show how it can be used to
+        average the point data across merged points.
+
+        >>> import pyvista as pv
+        >>> from pyvista import examples
+        >>> hexbeam = examples.load_hexbeam()
+        >>> hexbeam_shifted = hexbeam.translate([1, 0, 0])
+        >>> hexbeam.point_data['data'] = [0] * hexbeam.n_points
+        >>> hexbeam_shifted.point_data['data'] = [1] * hexbeam.n_points
+        >>> merged = hexbeam.merge(hexbeam_shifted, merge_points=False)
+        >>> cleaned = merged.clean(average_point_data=True)
+        >>> cleaned.n_points < merged.n_points
+        True
+
+        Show how point averaging using the ``clean`` method with
+        ``average_point_data=True`` results in averaged point data for merged
+        points.
+
+        >>> pl = pv.Plotter(shape=(1, 2))
+        >>> _ = pl.add_mesh(merged, scalars='data', show_scalar_bar=False)
+        >>> pl.subplot(0, 1)
+        >>> _ = pl.add_mesh(cleaned, scalars='data')
+        >>> pl.show()
+
+        """
+        try:
+            from vtkmodules.vtkFiltersCore import vtkStaticCleanUnstructuredGrid
+        except ImportError:  # pragma no cover
+            raise VTKVersionError("UnstructuredGrid.clean requires VTK >= 9.2.2") from None
+
+        alg = vtkStaticCleanUnstructuredGrid()
+        alg.SetInputDataObject(self)
+        alg.SetAbsoluteTolerance(True)
+        alg.SetTolerance(tolerance)
+        alg.SetMergingArray(merging_array_name)
+        alg.SetRemoveUnusedPoints(remove_unused_points)
+        alg.SetProduceMergeMap(produce_merge_map)
+        alg.SetAveragePointData(average_point_data)
+        _update_alg(alg, progress_bar, 'Cleaning Unstructured Grid')
+        return _get_output(alg)
diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py
index 16392fc7..509ae5cf 100644
--- a/pyvista/plotting/plotting.py
+++ b/pyvista/plotting/plotting.py
@@ -4866,7 +4866,15 @@ class BasePlotter(PickingHelper, WidgetHelper):
             filename = os.path.join(pyvista.FIGURE_PATH, filename)
         self.mwriter = get_writer(filename, fps=framerate, quality=quality, **kwargs)
 
-    def open_gif(self, filename, loop=0, fps=10, palettesize=256, subrectangles=False, **kwargs):
+    def open_gif(
+        self,
+        filename,
+        loop=0,
+        fps=10,
+        palettesize=256,
+        subrectangles=False,
+        **kwargs,
+    ):
         """Open a gif file.
 
         Requires ``imageio`` to be installed.
@@ -4920,7 +4928,7 @@ class BasePlotter(PickingHelper, WidgetHelper):
 
         """
         try:
-            from imageio import get_writer
+            from imageio import __version__, get_writer
         except ModuleNotFoundError:  # pragma: no cover
             raise ModuleNotFoundError(
                 'Install imageio to use `open_gif` with:\n\n   pip install imageio'
@@ -4931,15 +4939,17 @@ class BasePlotter(PickingHelper, WidgetHelper):
         if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename):
             filename = os.path.join(pyvista.FIGURE_PATH, filename)
         self._gif_filename = os.path.abspath(filename)
-        self.mwriter = get_writer(
-            filename,
-            mode='I',
-            loop=loop,
-            fps=fps,
-            palettesize=palettesize,
-            subrectangles=subrectangles,
-            **kwargs,
-        )
+
+        kwargs['mode'] = 'I'
+        kwargs['loop'] = loop
+        kwargs['palettesize'] = palettesize
+        kwargs['subrectangles'] = subrectangles
+        if scooby.meets_version(__version__, '2.28.1'):
+            kwargs['duration'] = 1000 * 1 / fps
+        else:  # pragma: no cover
+            kwargs['fps'] = fps
+
+        self.mwriter = get_writer(filename, **kwargs)
 
     def write_frame(self):
         """Write a single frame to the movie file.


Test patch:
diff --git a/requirements_test.txt b/requirements_test.txt
index 4e894730..8009d545 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -4,7 +4,7 @@
 cmocean<3.1
 colorcet<3.1.0
 hypothesis<6.74.2
-imageio<2.28.0
+imageio<2.29.0
 imageio-ffmpeg<0.5.0
 ipygany<0.6.0
 ipython<9.0.0
diff --git a/tests/filters/test_unstructured_grid.py b/tests/filters/test_unstructured_grid.py
new file mode 100644
index 00000000..a352617f
--- /dev/null
+++ b/tests/filters/test_unstructured_grid.py
@@ -0,0 +1,73 @@
+"""Tests for UnstructuredGridFilters."""
+
+import numpy as np
+import pytest
+
+import pyvista as pv
+
+skip_lesser_9_2_2 = pytest.mark.skipif(
+    pv.vtk_version_info <= (9, 2, 2), reason='Requires VTK>=9.2.2'
+)
+
+
+@skip_lesser_9_2_2
+def test_clean_points():
+    """Test on a set of points."""
+    n_unique_points = 100
+    u_points = np.random.random((n_unique_points, 3))
+    points = np.vstack([u_points] * 2)
+
+    grid = pv.PolyData(points).cast_to_unstructured_grid()
+    grid.point_data['data'] = np.hstack((np.zeros(n_unique_points), np.ones(n_unique_points)))
+
+    cleaned = grid.clean(produce_merge_map=True)
+    assert cleaned.field_data['PointMergeMap'].size == points.shape[0]
+    assert cleaned.n_points == n_unique_points
+    assert np.allclose(cleaned.point_data['data'], 0.5)
+    assert cleaned.point_data['data'].size == n_unique_points
+
+    # verify not averaging works
+    cleaned = grid.clean(average_point_data=False)
+    assert cleaned.n_points == n_unique_points
+    assert not (cleaned.point_data['data'] == 0.5).any()
+
+    # verify tolerance
+    u_points = np.random.random((n_unique_points, 3))
+    u_points_shift = u_points.copy()
+    u_points_shift[:, 2] += 1e-4
+    points = np.vstack((u_points, u_points_shift))
+
+    grid = pv.PolyData(points).cast_to_unstructured_grid()
+    cleaned = grid.clean(tolerance=1e-5)
+    assert cleaned.n_points == points.shape[0]
+
+
+@skip_lesser_9_2_2
+def test_clean_grid(hexbeam):
+    hexbeam_shifted = hexbeam.translate([1, 0, 0])
+
+    hexbeam.point_data['data'] = np.zeros(hexbeam.n_points)
+    hexbeam_shifted.point_data['data'] = np.ones(hexbeam.n_points)
+
+    merged = hexbeam.merge(hexbeam_shifted, merge_points=False)
+    cleaned = merged.clean(average_point_data=True, produce_merge_map=False)
+    assert 'PointMergeMap' not in cleaned.field_data
+
+    # expect averaging for all merged nodes
+    n_merged = merged.n_points - cleaned.n_points
+    assert (cleaned['data'] == 0.5).sum() == n_merged
+
+    cleaned = merged.clean(average_point_data=False)
+    assert not (cleaned['data'] == 0.5).any()
+
+    # test merging_array
+    cleaned = merged.clean(average_point_data=True, merging_array_name='data')
+    assert cleaned.n_points == hexbeam.n_points * 2
+
+    hexbeam.point_data['data_2'] = np.ones(hexbeam.n_points)
+    hexbeam_shifted.point_data['data_2'] = np.ones(hexbeam.n_points)
+    merged = hexbeam.merge(hexbeam_shifted, merge_points=False)
+
+    # test merging_array
+    cleaned = merged.clean(average_point_data=True, merging_array_name='data_2')
+    assert cleaned.n_points == 165
diff --git a/tests/utilities/test_misc.py b/tests/utilities/test_misc.py
index 69d7f8d8..bdaae28a 100644
--- a/tests/utilities/test_misc.py
+++ b/tests/utilities/test_misc.py
@@ -1,5 +1,6 @@
 import os
 
+import numpy as np
 import pytest
 
 from pyvista import examples
@@ -29,4 +30,4 @@ def test_has_module():
 @pytest.mark.skipif(not HAS_IMAGEIO, reason="Requires imageio")
 def test_try_imageio_imread():
     img = _try_imageio_imread(examples.mapfile)
-    assert isinstance(img, imageio.core.util.Array)
+    assert isinstance(img, (imageio.core.util.Array, np.ndarray))