mikedh / trimesh

Python library for loading and using triangular meshes.
https://trimesh.org
MIT License
2.85k stars 567 forks source link

A few new failing tests with Pillow 10.4.0 #2247

Open musicinmybrain opened 2 days ago

musicinmybrain commented 2 days ago

I see a few test regressions that appear to be associated with the update from Pillow 10.3.0 to 10.4.0:

$ gh repo clone mikedh/trimesh
$ cd trimesh
$ python3.12 -m venv _e
$ . _e/bin/activate
(_e) $ pip install -e .[all,test]
(_e) $ python -m pytest -v
=========================================================================================== short test summary info ============================================================================================
FAILED tests/test_boolean.py::BooleanTest::test_boolean - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmp8kugzuuk']' returned non-zero exit status 127.
FAILED tests/test_boolean.py::BooleanTest::test_empty - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmpuim2cl9q']' returned non-zero exit status 127.
FAILED tests/test_boolean.py::BooleanTest::test_multiple - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmpcl7n6omm']' returned non-zero exit status 127.
FAILED tests/test_creation.py::CreationTest::test_path_sweep - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_creation.py::CreationTest::test_triangulate - AssertionError: assert np.False_
FAILED tests/test_creation.py::CreationTest::test_triangulate_plumbing - AssertionError: assert np.False_
FAILED tests/test_gltf.py::GLTFTest::test_same_name - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
FAILED tests/test_gltf.py::GLTFTest::test_vertex_colors_import - AssertionError: Imported vertex color is not of expected value: got [  1   0   1 255], expected [255   0 255 255]
FAILED tests/test_medial.py::MedialTests::test_medial - OverflowError: Python int too large to convert to C long
FAILED tests/test_paths.py::VectorTests::test_discrete - OverflowError: Python int too large to convert to C long
FAILED tests/test_paths.py::VectorTests::test_empty - OverflowError: Python int too large to convert to C long
FAILED tests/test_section.py::SliceTest::test_cap - assert False
FAILED tests/test_section.py::SliceTest::test_cap_coplanar - assert False
FAILED tests/test_section.py::SliceTest::test_slice_onplane - assert False
FAILED tests/test_sweep.py::test_simple_closed - AssertionError
FAILED tests/test_sweep.py::test_simple_extrude - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_simple_open - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_spline_3D - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_screw - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_texture.py::TextureTest::test_pbr_material_fusion - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
FAILED tests/test_texture.py::TextureTest::test_upsize - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
FAILED tests/test_unwrap.py::UnwrapTest::test_blender_unwrap - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmpr4ccikox']' returned non-zero exit status 127.
=========================================================================== 22 failed, 598 passed, 274 warnings in 290.71s (0:04:50) ===========================================================================

Now, downgrading Pillow:

(_e) $ pip install pillow==10.3.0
(_e) $ python -m pytest -v
=========================================================================================== short test summary info ============================================================================================
FAILED tests/test_boolean.py::BooleanTest::test_boolean - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmpl559s849']' returned non-zero exit status 127.
FAILED tests/test_boolean.py::BooleanTest::test_empty - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmp81bp1hok']' returned non-zero exit status 127.
FAILED tests/test_boolean.py::BooleanTest::test_multiple - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmpt_hre7jb']' returned non-zero exit status 127.
FAILED tests/test_creation.py::CreationTest::test_path_sweep - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_creation.py::CreationTest::test_triangulate - AssertionError: assert np.False_
FAILED tests/test_creation.py::CreationTest::test_triangulate_plumbing - AssertionError: assert np.False_
FAILED tests/test_gltf.py::GLTFTest::test_vertex_colors_import - AssertionError: Imported vertex color is not of expected value: got [  1   0   1 255], expected [255   0 255 255]
FAILED tests/test_medial.py::MedialTests::test_medial - OverflowError: Python int too large to convert to C long
FAILED tests/test_paths.py::VectorTests::test_discrete - OverflowError: Python int too large to convert to C long
FAILED tests/test_paths.py::VectorTests::test_empty - OverflowError: Python int too large to convert to C long
FAILED tests/test_section.py::SliceTest::test_cap - assert False
FAILED tests/test_section.py::SliceTest::test_cap_coplanar - assert False
FAILED tests/test_section.py::SliceTest::test_slice_onplane - assert False
FAILED tests/test_sweep.py::test_simple_closed - AssertionError
FAILED tests/test_sweep.py::test_simple_extrude - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_simple_open - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_spline_3D - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_sweep.py::test_screw - ValueError: zero-size array to reduction operation maximum which has no identity
FAILED tests/test_unwrap.py::UnwrapTest::test_blender_unwrap - subprocess.CalledProcessError: Command '['/usr/bin/blender', '--background', '--python', '/tmp/tmp03x5p5xr']' returned non-zero exit status 127.
=========================================================================== 19 failed, 601 passed, 273 warnings in 292.99s (0:04:52) ===========================================================================

The regressions of interest here are therefore those failures that appear only in the first list:

FAILED tests/test_gltf.py::GLTFTest::test_same_name - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
FAILED tests/test_texture.py::TextureTest::test_pbr_material_fusion - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
FAILED tests/test_texture.py::TextureTest::test_upsize - ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

There isn’t really anything obviously relevant in the “curated” upstream changelog; meanwhile the raw list of changes is too verbose to easily peruse. I’ll try to keep investigating this.

musicinmybrain commented 2 days ago

Sample detailed output from a failing test:

___________________________________________________________________________________________ GLTFTest.test_same_name ____________________________________________________________________________________________

self = <tests.test_gltf.GLTFTest testMethod=test_same_name>

    def test_same_name(self):
        s = g.get_mesh("TestScene.gltf")
        # hardcode correct bounds to check against
>       bounds = s.dump(concatenate=True).bounds

tests/test_gltf.py:745:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
trimesh/scene/scene.py:893: in dump
    return util.concatenate(result)
trimesh/util.py:1499: in concatenate
    raise E
trimesh/util.py:1494: in concatenate
    visual = is_mesh[0].visual.concatenate([m.visual for m in is_mesh[1:]])
trimesh/visual/texture.py:216: in concatenate
    return concatenate(self, others)
trimesh/visual/objects.py:80: in concatenate
    new_mat, new_uv = pack(materials=mats, uvs=uvs)
trimesh/visual/material.py:1014: in pack
    images = resize_images(images, unpadded_sizes)
trimesh/visual/material.py:934: in resize_images
    img = img.resize(size)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <PIL.Image.Image image mode=RGBA size=2x2 at 0x7FCD6A87A540>, size = array([2, 2]), resample = <Resampling.BICUBIC: 3>, box = (0, 0, 2, 2), reducing_gap = None

    def resize(
        self,
        size: tuple[int, int],
        resample: int | None = None,
        box: tuple[float, float, float, float] | None = None,
        reducing_gap: float | None = None,
    ) -> Image:
        """
        Returns a resized copy of this image.

        :param size: The requested size in pixels, as a 2-tuple:
           (width, height).
        :param resample: An optional resampling filter.  This can be
           one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
           :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
           :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
           If the image has mode "1" or "P", it is always set to
           :py:data:`Resampling.NEAREST`. If the image mode specifies a number
           of bits, such as "I;16", then the default filter is
           :py:data:`Resampling.NEAREST`. Otherwise, the default filter is
           :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
        :param box: An optional 4-tuple of floats providing
           the source image region to be scaled.
           The values must be within (0, 0, width, height) rectangle.
           If omitted or None, the entire source is used.
        :param reducing_gap: Apply optimization by resizing the image
           in two steps. First, reducing the image by integer times
           using :py:meth:`~PIL.Image.Image.reduce`.
           Second, resizing using regular resampling. The last step
           changes size no less than by ``reducing_gap`` times.
           ``reducing_gap`` may be None (no first step is performed)
           or should be greater than 1.0. The bigger ``reducing_gap``,
           the closer the result to the fair resampling.
           The smaller ``reducing_gap``, the faster resizing.
           With ``reducing_gap`` greater or equal to 3.0, the result is
           indistinguishable from fair resampling in most cases.
           The default value is None (no optimization).
        :returns: An :py:class:`~PIL.Image.Image` object.
        """

        if resample is None:
            type_special = ";" in self.mode
            resample = Resampling.NEAREST if type_special else Resampling.BICUBIC
        elif resample not in (
            Resampling.NEAREST,
            Resampling.BILINEAR,
            Resampling.BICUBIC,
            Resampling.LANCZOS,
            Resampling.BOX,
            Resampling.HAMMING,
        ):
            msg = f"Unknown resampling filter ({resample})."
            filters = [
                f"{filter[1]} ({filter[0]})"
                for filter in (
                    (Resampling.NEAREST, "Image.Resampling.NEAREST"),
                    (Resampling.LANCZOS, "Image.Resampling.LANCZOS"),
                    (Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
                    (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
                    (Resampling.BOX, "Image.Resampling.BOX"),
                    (Resampling.HAMMING, "Image.Resampling.HAMMING"),
                )
            ]
            msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}"
            raise ValueError(msg)

        if reducing_gap is not None and reducing_gap < 1.0:
            msg = "reducing_gap must be 1.0 or greater"
            raise ValueError(msg)

        self.load()
        if box is None:
            box = (0, 0) + self.size

>       if self.size == size and box == (0, 0) + self.size:
E       ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

_e/lib64/python3.12/site-packages/PIL/Image.py:2297: ValueError
musicinmybrain commented 2 days ago

The key difference seems to be this.

With numpy==2.0.0, pillow==10.3.0:

>>> from PIL import Image
>>> from numpy import array
>>> x = Image.new('RGB', (2, 4))
>>> x.resize(array([3, 7]))
<PIL.Image.Image image mode=RGB size=3x7 at 0x7FB58A5866C0>

With numpy==2.0.0, pillow==10.4.0:

>>> from PIL import Image
>>> from numpy import array
>>> x = Image.new('RGB', (2, 4))
>>> x.resize(array([3, 7]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ben/fedora/neuro-sig/python-trimesh/_e/lib64/python3.12/site-packages/PIL/Image.py", line 2297, in resize
    if self.size == size and box == (0, 0) + self.size:
       ^^^^^^^^^^^^^^^^^
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

So the question is whether this is a regression in Pillow, or whether passing a numpy.ndarray to Image.resize() was always “wrong,” since https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.resize calls for a “tuple,” and it only happened to work.