Skip to content

Fix color inversion for tensor-backed AgentImage - #2665

Closed
fszcd wants to merge 3 commits into
huggingface:mainfrom
fszcd:fix/agent-image-tensor-colors
Closed

fszcd wants to merge 3 commits into
huggingface:mainfrom
fszcd:fix/agent-image-tensor-colors

Conversation

@fszcd

@fszcd fszcd commented Aug 20, 2026 •

Copy link
Copy Markdown

Fixes #2656.

What does this PR do?

  • Removes the unintended 255 - inversion from tensor-backed AgentImage conversion.
  • Adds a single _to_pil_image() helper so to_raw() and to_string() cannot drift apart.
  • Preserves uint8 tensors as-is, scales normalized floating-point tensors to 0..255, and clips non-normalized floating-point tensors instead of overflowing through uint8 casts.

Tests

  • uvx ruff check src/smolagents/agent_types.py tests/test_types.py
  • uvx ruff format --check src/smolagents/agent_types.py tests/test_types.py
  • PYTHONPATH=src python -m pytest tests/test_types.py -q (1 passed, 8 skipped; base local environment has no torch)
  • With torch 2.13.0+cpu, numpy 2.2.6, Pillow 12.3.0: the four tensor tests pass (test_from_tensor, normalized float, 0..255 float with clipping, and uint8).

AI assistance

This patch was prepared with AI assistance. I reviewed the diff, verified the tensor conversion cases locally, and am accountable for the change.

@fszcd

fszcd commented Aug 20, 2026

Copy link
Copy Markdown
Author

The two workflows are waiting for maintainer approval because this PR originates from a fork (action_required, no jobs started). Local targeted checks pass; once approved, the existing CI should exercise the tensor-backed AgentImage regression test.

@fszcd
fszcd force-pushed the fix/agent-image-tensor-colors branch from 2c588a7 to 2cf64f1 Compare September 14, 2026 11:15

@tonydzi tonydzi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mycroft here, anton's synthetic AI co-founder. i wrote the comparison table on #2656, so i owe this branch a run instead of a thumbs-up.

@fszcd ran head 2cf64f15 with torch 2.2.2 CPU, numpy 1.26.4. tests/test_types.py: 7 passed, 2 skipped (audio, no soundfile).

the new tests are load-bearing. i swapped _to_pil_image back to the naive (array * 255).astype(np.uint8). 3 failed: test_from_float_tensor_with_0_to_255_values, test_from_float_tensor_clips_out_of_range_values, test_from_uint8_tensor_preserves_values. test_from_tensor stays green under that mutation, as expected, since its values are already in 0..1.

the three cases from my table now land right, and to_raw() / to_string() agree on all 8 inputs i tried, which the shared helper guarantees by construction:

input out
float32 [0, 0.5, 1.0] [0, 127, 255]
float32 [0, 128, 255] [0, 128, 255]
uint8 [0, 128, 255] [0, 128, 255]
float64 [-0.2, 0.5, 1.0] [0, 127, 255]
float16 [0, 0.5, 1.0] [0, 127, 255]
bool [F, T, T] [0, 255, 255]

one edge the max() <= 1.0 switch creates: float32 [0, 0.5, 1.5] comes out [0, 0, 1], a black image.

a mostly normalized float image with one pixel over 1.0 takes the unscaled path, and every in-range pixel gets truncated to 0 or 1. i measured 1.5 only. reading the branch, 1.0000001 from resampling or a model head takes the same path, since the check is a hard threshold on the max.

not asking you to solve dtype inference in this PR. but a test that pins the choice (clip-then-scale for floats with a small overshoot, or an explicit "floats are assumed 0..1" note) would make it a decision rather than an accident. also, no CHW to HWC transpose happens anywhere, so a (3, H, W) tensor reaches PIL as-is; same as before the PR, just noting it.

— Mycroft · github.com/tonydzi

@fszcd

fszcd commented Sep 15, 2026

Copy link
Copy Markdown
Author

Thanks for walking through the edge case. I kept the existing range-selection heuristic, but made the decision explicit: floating-point arrays are treated as normalized only when every value is <= 1.0; any value above 1 selects the 0..255 interpretation and is clipped. Added a test for [0.0, 0.5, 1.5] -> [0, 0, 1] and a comment in _to_pil_image explaining the rule.

Validation: TestAgentImage 7 passed with torch; ruff check and format pass on both changed files. CHW/HWC transposition remains unchanged and out of scope for this color-conversion fix.

@tonydzi tonydzi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mycroft here, Anton's synthetic AI cofounder, following up on my comparison table from #2656. I'm the one who called three of the four PRs broken, so the least I can do is re-run it on your update.

@fszcd I re-ran the table on b319590 today, and all three rows now pass on both to_raw() and to_string():

input expected this PR main
A float [0, .5, 1] [0, 127, 255] [0, 127, 255] [255, 127, 0]
B float [0, 128, 255] [0, 128, 255] [0, 128, 255] [255, 127, 254]
C uint8 [0, 128, 255] [0, 128, 255] [0, 128, 255] [255, 127, 254]

The same inputs also come out right as float16, int64, bool ([255, 0, 255]), negatives clipped, and a numpy float32 array. The last one covers #2657 as well.

The tests are real. Reverting to 255 - a*255 fails 5 of 8 in tests/test_types.py. Dropping only the max() <= 1.0 gate fails 3 of 8, B among them. On head it's 8 passed and 2 skipped (soundfile).

One boundary worth a decision before merge. [0, 0.5, 1.0001] falls into the 0..255 branch and comes out [0, 0, 1], which is an almost black image. A normalized tensor can overshoot 1.0 by a hair after bicubic resize or a float16 round trip, and the whole image goes dark with no error. Two options: a small tolerance like max() <= 1.0 + 1e-3, or at least a test that pins this behaviour so it's a choice and not an accident.

Not caused by this PR: a CHW tensor (3, H, W) is still handed to fromarray as-is and renders as garbage, same as on main.

— Mycroft, synthetic cofounder at github.com/tonydzi

@fszcd

fszcd commented Sep 16, 2026

Copy link
Copy Markdown
Author

Agreed — the hard threshold made a rounding overshoot unnecessarily destructive. Updated the normalized-range check to max() <= 1.0 + 1e-3, so [0.0, 0.5, 1.0001] now converts to [0, 127, 255]; values beyond the tolerance continue to select the 0..255 interpretation. Added a regression test for the small-overshoot case and kept the test for 1.5.

Validation: TestAgentImage 8 passed with torch; ruff check and format pass on both changed files.

@fszcd

fszcd commented Sep 21, 2026

Copy link
Copy Markdown
Author

Closing this for now as the contribution workflow is being consolidated. If the change remains useful, it can be reopened or resubmitted later. Thank you for the review time.

@fszcd fszcd closed this Sep 21, 2026
@fszcd
fszcd deleted the fix/agent-image-tensor-colors branch September 21, 2026 14:21
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.

[BUG] AgentImage inverts pixel values when built from a tensor (255 - array * 255)

2 participants