# Pillow Autocontrast Mask Still Changes Outside the Selection

Disclosure: This article was generated with AI for the FrameSprite team, which has a commercial interest in FrameSprite. It received a separate AI editorial review against the cited sources. Executed synthetic fixtures are identified with their runtime and limits; no other editor, engine, production or performance test is implied.

Use a second compositing step if only a selected region may receive Pillow's autocontrast result. The `mask` argument to `ImageOps.autocontrast` selects pixels for the histogram calculation. It does not restrict where the resulting tonal lookup table is applied. Calculate the adjustment using a statistical mask, then write it through a separate output mask.

> **Key Takeaways**
>
> - Name the histogram-selection mask separately from the allowed-write mask.
> - Composite the adjusted result over the original to protect outside pixels.
> - Check both that the selection changed and that the protected region did not.

## Why the untouched border changes

Suppose an opaque grayscale strip contains `[20, 60, 100, 140, 180, 220]`. You want to increase contrast only in the middle pair, without altering the surrounding reference marks. A binary mask `[0, 0, 255, 255, 0, 0]` selects the samples 100 and 140.

The [autocontrast reference](https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.autocontrast) states that the histogram is computed using pixels within the mask. The [maintained implementation](https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageOps.py) calculates that masked histogram, builds the lookup table, and applies it to the image. A histogram boundary is therefore different from an output boundary.

With the fixture's default zero cutoff and `preserve_tone=False`, the selected endpoints map to 0 and 255. Applying that mapping to the full strip produced `[0, 0, 0, 255, 255, 255]`. All four outside samples changed, even though none contributed to the histogram.

![Executed grayscale strip before adjustment, after histogram-mask-only adjustment, and after protecting the outside region with a write mask](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture.webp)

The general protected-region requirement also appears in the [local outfit-edit workflow](https://www.framesprite.com/guides/ai-character-outfit-edit-mask). This task has a different cause: a deterministic histogram API uses its mask for statistics. No generated replacement image or character-edit interface is involved.

## Calculate first, constrain the write second

Use two names even when both masks currently contain the same binary pixels:

```python
from PIL import Image, ImageOps

original = Image.new("L", (6, 1))
original.putdata([20, 60, 100, 140, 180, 220])

statistics_mask = Image.new("L", original.size)
statistics_mask.putdata([0, 0, 255, 255, 0, 0])
write_mask = statistics_mask.copy()

adjusted = ImageOps.autocontrast(original, mask=statistics_mask)
local = Image.composite(adjusted, original, write_mask)
local.save("pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png", format="PNG")

with Image.open("pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png") as delivered:
    delivered.load()
    assert delivered.mode == "L"
    assert list(delivered.get_flattened_data()) == [20, 60, 0, 255, 180, 220]
```

The [composite reference](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.composite) explains the second operation: the mask combines the first and second images. Here white selects `adjusted`, while black selects `original`. Image sizes and the compositing mask must agree. This example uses `L` images and a binary `L` mask throughout; it is not an RGBA source-over or transparency tutorial.

The masks can later differ on purpose. You might derive contrast from a representative interior patch but permit the adjustment over a larger repair region. Keep that decision explicit. This fixture does not test feathered masks; introducing intermediate write-mask values creates a blended transition rather than exact selection of one input at every pixel.

## Test protection and useful change together

An unchanged protected region is necessary, but returning the original image everywhere would also satisfy that condition. Verify the selected region as well:

| Check | Executed result |
| --- | --- |
| Histogram-only changes outside indices | `0, 1, 4, 5` |
| Constrained output changes outside indices | None |
| Selected samples after constrained output | `0, 255` |
| Selected samples actually changed | Both |

All nine checks passed on Python 3.12.10 and Pillow 13.0.0 on Windows. The script saved and reopened the constrained PNG and verified exact decoded samples. Its empty statistical-mask and constant statistical-region controls both returned the unchanged strip in this version; they do not demonstrate a useful contrast increase. For production work, reject or report an empty selection instead of counting it as an editing success.

Compare decoded output pixels when testing protection. A different compressed file hash alone does not identify a changed region; see the separate [file-hash versus decoded-pixel identity guide](https://www.framesprite.com/guides/sprite-png-file-hash-versus-decoded-pixel-identity). If your later pipeline adds transparency, perform its separate [transparent-pixel RGB review](https://www.framesprite.com/guides/sprite-animation-transparent-pixel-rgb-qa); this opaque grayscale test makes no alpha-preservation claim.

## Retain the reproducible repair

Download the [original strip](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-contrast-source.png), [histogram-only result](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-histogram-mask-only.png), [constrained result](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-write-mask-constrained.png), [input masks and settings](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture-input.json), and [execution record](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-fixture-results.json). The independent [fixture script](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region-supplement-fixtures.py) runs only this bounded case with:

```sh
python pillow-autocontrast-statistics-mask-output-region-supplement-fixtures.py --out-root ./reproduced --only pillow-autocontrast-statistics-mask-output-region
```

The evidence establishes statistical selection versus write selection for these synthetic `L` pixels. It does not establish seam quality in a painted image, production compatibility, or results in a GUI editor.

Prepared by the [FrameSprite Workflow Team](https://www.framesprite.com/about#workflow-team), which has a commercial relationship with this site. AI-assisted technical writing using original synthetic data, official API references, and an executed export; no generative editor or GUI was tested. [Editorial method](https://www.framesprite.com/about) · [Corrections](https://www.framesprite.com/contact).

[Original guide and downloadable materials](https://www.framesprite.com/guides/pillow-autocontrast-statistics-mask-output-region)

