Pillow >=12 breaks pil_to_numpy() via private PIL.Image API arity change
Description
nerfstudio/data/utils/data_utils.py's pil_to_numpy() calls PIL.Image._getencoder(...).setimage(im.im) directly — a private, undocumented API used as a fast-path replacement for Image.tobytes(). Pillow 12 changed the arity/signature of this private encoder API, so this call breaks with a fresh Pillow install.
pyproject.toml's Pillow>=10.3.0 pin is open-ended, so a plain pip install nerfstudio (or any fresh environment resolve) can pull in Pillow 12 and hit this immediately — it's not a hypothetical edge case, it broke a from-scratch install during ROCm porting work on this fork.
Reproduction
Fresh install with no upper Pillow bound resolves to Pillow >=12, then any code path touching pil_to_numpy() (mask loading, at minimum — see call site at data_utils.py:68) raises at that call.
Root cause
Image._getencoder(im.mode, "raw", im.mode) followed by e.setimage(im.im) bypasses PIL's own public tobytes()/frombytes() path, presumably for performance (avoiding an intermediate bytes copy). Pillow 12's internal encoder object's setimage() no longer accepts the same arguments this code assumes.
Suggested fix
Two options, in order of preference:
- A source-level fix: reimplement
pil_to_numpy()against Pillow 12's actual current private encoder signature (need to check exactly what changed), or drop the private-API fast path entirely in favor ofnp.asarray(im)/im.tobytes()+np.frombuffer(...), accepting whatever performance difference that has. - A minimal mitigation: tighten the
pyproject.tomlpin toPillow<12until a real fix lands, following the precedent of past Pillow-compatibility pins in this repo (e.g. #3034, #2446).
We worked around this in our own fork with a Pillow<12 pin at the Docker image level (not in pyproject.toml itself), which isn't upstreamable as a real fix — flagging this here rather than opening a PR with just the same band-aid, since a maintainer may prefer the source-level fix.
Source: nerfstudio-project/nerfstudio