Skip to content

Support for nested image lists (multi-view arrays) and improvements on image file handling - #1

Open
dalgarak wants to merge 4 commits into
lhoestq:mainfrom
dalgarak:main
Open

Support for nested image lists (multi-view arrays) and improvements on image file handling#1
dalgarak wants to merge 4 commits into
lhoestq:mainfrom
dalgarak:main

Conversation

@dalgarak

Copy link
Copy Markdown

Feat: Support for nested image lists (multi-view arrays) & improvements on image file handling

Motivation & Context

When working with multi-view datasets, such as the InternScience/SFE dataset, a single DataFrame cell often contains an array of image paths (e.g., ['front.png', 'side.png', 'top.png']).

While fixing the string paths themselves can be done with simple mapping operations, the original library strictly fails to generate an image-embedded Parquet file from these nested lists, throwing an ArrowTypeError. The primary motivation of this PR is to fix the core serialization pipeline so that saving and loading image-embedded Parquet files works flawlessly for multi-view data.

Changes

The following functions were refactored to support recursive processing of list and np.ndarray structures and hf dataset compatibility:

  • _encode_pil_image / _decode_pil_image
  • ImageArray.arrow_array, ImageArray.feature
  • ImageArray._from_sequence / ImageArray._from_sequence_of_strings
  • PILMethods.enable
  • PILMethods._apply

Supplementary Improvements

  • html_formatter: Supports horizontal rendering for multiple thumbnails.
  • Resource Safety: Implemented img.copy() in loading logic to prevent "Too many open files" errors.

- Refactor `ImageArray._from_sequence` and `_from_sequence_of_strings` to recursively process and preserve `list` and `np.ndarray` structures.
- Prevent "Too many open files" OS errors by enforcing immediate file handle closure via `img.copy()` during load.
- Fix `ArrowTypeError` in Parquet serialization by introducing dynamic PyArrow schema resolution (`pa.list_`) and recursive encoding/decoding in `_encode_pil_image` and `_decode_pil_image`.
- Update `ImageArray.feature` to dynamically return Hugging Face `Sequence` type for multi-view data.
- Fix `PILMethods.enable` to prevent truncation of lists into single elements when wrapping back into a pandas Series.
- Enhance `html_formatter` to render multiple image thumbnails horizontally within Jupyter Notebooks when dealing with nested lists.
@lhoestq

lhoestq commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Hi ! In this PR I undertsand you are adding df["images"].pil.xxx, and I'm wondering if we can actually generalize this to any kind of nesting, e.g. df["images"].list.pil.xxx (or on dicts df["image_and_text"].dict["image"].pil.xxx)

Maybe we can focus first on your use case for lists

import pandas as pd
from pandas_image_methods import PILMethods, ListMethods

pd.api.extensions.register_series_accessor("pil")(PILMethods)
pd.api.extensions.register_series_accessor("list")(ListMethods)

df = pd.DataFrame({"file_paths": [["path/to/image.png"]]})
df["images"] = df["file_paths"].list.pil.open()
df["images"] = df["image"].list.pil.rotate(90)
# 0    [<PIL.Image.Image size=200x200>]
# Name: images, dtype: object, List methods enabled

wdyt ?

Otherwise I'm a bit concerned about the ambiguities of appliying pil.xxx on nested objects that could contain objects that are not images

…guity, and reverts _encode_pil/_decode_pil_images (2) add ListMethods, DictMethods over Nested PILMethods Proxy (3) remove auto-registration
@dalgarak

Copy link
Copy Markdown
Author

LGTM! Thanks for the suggestion. I've implemented ListMethods and DictMethods as you suggested.

Here's the summary of the changes:

  • Reverted changes to the original PILMethods to keep it simple.
  • Introduced a Proxy pattern to generalize nested structures. Now df.list.pil.xxx and df.dict.pil.xxx work by mapping any PILMethods to the leaf elements.
  • Separated responsibilities: Accessors now handle the "nesting" while PILMethods focuses on images. Added strict type checks to clear up any ambiguity—it'll throw a TypeError if non-image data is found inside.
  • Modified ImageArray to handle type guidance for Arrow/Parquet, which was necessary for nested serialization.

I noticed a UserWarning when overriding the default pandas .list attribute, but it's working as intended. Also confirmed that Dataset.from_pandas(df) correctly recognizes the features as List[Image].

tested with:

import pandas as pd
import numpy as np
from pandas_image_methods import PILMethods, ListMethods, DictMethods
from datasets import Dataset, load_dataset, Image

pd.api.extensions.register_series_accessor("pil")(PILMethods)
pd.api.extensions.register_series_accessor("list")(ListMethods)
pd.api.extensions.register_series_accessor("dict")(DictMethods)

df = pd.DataFrame({"file_paths": [["./images/file_a.png", "./images/file_b.png"],]})

def to_dict(x):
    if isinstance(x, (list, np.ndarray)):
        return {f"img{i+1}": file_path for i, file_path in enumerate(x)}
    return x
df["dict_paths"] = df["file_paths"].apply(to_dict)
print(df["file_paths"])

try:
    df["images"] = df["file_paths"].pil.open()
except AttributeError as e:
    print(e)

try:
    df["dict_images"] = df["dict_paths"].pil.open()
except AttributeError as e:
    print(e)

df["images"] = df["file_paths"].list.pil.open()
print(df["images"])
df["dict_images"] = df["dict_paths"].dict.pil.open()
print(str(df["dict_images"]))

ds = Dataset.from_pandas(df)
print(ds.features)

expected results:

UserWarning: registration of accessor <class 'pandas_image_methods.core.ListMethods'> under name 'list' for type <class 'pandas.core.series.Series'> is overriding a preexisting attribute with the same name.
  pd.api.extensions.register_series_accessor("list")(ListMethods)
0    [./images/file_a.png, ./images/file_b.png]
Name: file_paths, dtype: object
PIL methods are not available on this Series. If your data is nested, use '.list.pil' or '.dict.pil' instead. Otherwise, call '.pil.enable()' first.
PIL methods are not available on this Series. If your data is nested, use '.list.pil' or '.dict.pil' instead. Otherwise, call '.pil.enable()' first.
0    [<PIL.Image.Image size=1673x1715>, <PIL.Image....
Name: images, dtype: object, PIL methods enabled
0    {'img1': <PIL.Image.Image size=1673x1715>, 'im...
Name: dict_images, dtype: object, PIL methods enabled
{'file_paths': List(Value('string')), 'dict_paths': {'img1': Value('string'), 'img2': Value('string')}, 'images': List(Image(mode=None, decode=True)), 'dict_images': {'img1': Image(mode=None, decode=True), 'img2': Image(mode=None, decode=True)}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants