Repository: matplotlib/matplotlib
Issue category: Bug Report

Issue:
[Bug]: inconsistent ‘animation.FuncAnimation’ between display and save
### Bug summary

when i want to save images to gif,  it's inconsistent between display and save; 

It seems that the color information has been lost:

![temp](https://github.com/user-attachments/assets/f1a6c123-2533-4db0-8c14-4264cfe3345c)


### Code for reproduction

```Python
def animation_test():
    import matplotlib.pyplot as plt
    import matplotlib.animation as animation
    file = r'example.dat'
    num_frames = 72
    nx = 8
    ny = 9
    data = np.fromfile(file, np.float32).reshape(num_frames, ny, nx)

    fig, ax = plt.subplots()
    img = data[0,]
    # plt.imshow(img)
    vmax = 100
    vmin = 0
    h = ax.imshow(img, cmap=plt.get_cmap('CMRmap_r'), origin='lower', interpolation='none', vmin=vmin, vmax=vmax, animated=True)
    ax.set_xticks(range(nx))
    ax.set_xticklabels(range(1, nx + 1))
    ax.set_yticks(range(ny))
    ax.set_yticklabels(range(1, ny + 1))
    fig.tight_layout()

    def update(frame):
        img = data[frame, ]
        h.set_array(img)
        return h,

    # create animation
    interval = 100
    ani = animation.FuncAnimation(fig, update, frames=range(num_frames), interval=interval, blit=True)
    # ani = animation.FuncAnimation(fig, update, frames=frame_iter, interval=interval, blit=False, cache_frame_data=False)

    ani.save('example.gif', writer='pillow', fps=2, dpi=300)

    pass

if __name__ == '__main__':
    animation_test()
```


### Actual outcome

above picture -> right

### Expected outcome

above picture -> left

### Additional information

_No response_

### Operating system

win10

### Matplotlib Version

'3.4.2'

### Matplotlib Backend

_No response_

### Python version

3.7.10

### Jupyter version

_No response_

### Installation

pip


Hints:
Do you mind also including the data points that you plotted?
I updated the code and uploaded the data file:
[example.zip](https://github.com/user-attachments/files/17945028/example.zip)

Thank you. I was able to reproduce the behavior now. It does seem like a bug.

It may be because the PillowWriter is renormalizing the color values frame-by-frame instead of using the original normalization that is still there when you directly .show() the plot. In that case, keeping around a Normalization object that the PillowWriter can reference later would solve it. But I'll let the veterans figure decide if that's the issue.
Um, can you provide a preliminary solution? :)
So far I'm only good enough to triage bugs. :(
Without having debugged this exactly, my guess is that this is a fundamental limitation of gif. From https://en.wikipedia.org/wiki/GIF

> The format can contain up to [8 bits per pixel](https://en.wikipedia.org/wiki/8-bit_color), allowing a single image to reference its own [palette](https://en.wikipedia.org/wiki/Palette_(computing)) of up to 256 different colors chosen from the [24-bit](https://en.wikipedia.org/wiki/24-bit_color) [RGB color space](https://en.wikipedia.org/wiki/RGB_color_model). It can also represent multiple images in a file, which can be used for [animations](https://en.wikipedia.org/wiki/Animation), and allows a separate palette of up to 256 colors for each frame. These palette limitations make GIF less suitable for reproducing color photographs and other [images with color gradients](https://en.wikipedia.org/wiki/Image_gradient) but well-suited for simpler images such as graphics or logos with solid areas of color.
This seems to be specific to the Pillow writer; it looks similar to the expected result when using ffmpeg.
This is a similar issue to https://github.com/matplotlib/matplotlib/issues/25806; Pillow converts the RGBA image to P(alette) and loses some colours. This is due to the inherent limitations of the GIF format as @timhoffm has mentioned. See for example the upstream issue https://github.com/python-pillow/Pillow/issues/6832

I think your best bet is to either switch to ffmpeg, which does this better, or switch to a more flexible format like `webp`.
Hmm, actually it looks like we can help Pillow a little bit here. If the image doesn't contain any transparency, then we can convert it to `RGB` mode, and Pillow's conversion from that mode to `P` mode is a bit better.

Patch:
diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py
index 47f2f0f9515b..2be61284073a 100644
--- a/lib/matplotlib/animation.py
+++ b/lib/matplotlib/animation.py
@@ -492,8 +492,15 @@ def grab_frame(self, **savefig_kwargs):
         buf = BytesIO()
         self.fig.savefig(
             buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi})
-        self._frames.append(Image.frombuffer(
-            "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1))
+        im = Image.frombuffer(
+            "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1)
+        if im.getextrema()[3][0] < 255:
+            # This frame has transparency, so we'll just add it as is.
+            self._frame.append(im)
+        else:
+            # Without transparency, we switch to RGB mode, which converts to P mode a
+            # little better if needed (specifically, this helps with GIF output.)
+            self._frames.append(im.convert("RGB"))
 
     def finish(self):
         self._frames[0].save(


Test patch: