PillowWriter 导出的 GIF 倾斜:帧宽少算一个像素
Matplotlib3.11.1可能把8.2英寸、100dpi的帧宽截成819,而RGBA画布实际为820,导致按错误行宽解释像素。3.11.2在帧尺寸取整处加入浮点容差。
动画预览正常,PillowWriter 保存的 GIF 却像每行逐渐向一侧错开。若发生在 Matplotlib 3.11.1,尤其 Figure 使用非整数英寸尺寸,应检查原始 RGBA 缓冲区宽度与 writer 声明的帧宽是否差了一个像素。
一个明确组合是 figsize=(8.2, 7)、导出 dpi=100:画布实际为 820×700,而旧 frame_size 得到 819×700。Pillow 按较小宽度解释同一段连续像素,行边界便会错位。这与 GIF 播放速度或折线斜率无关。
为什么数学上 820 仍可能被截成 819
旧帧尺寸计算直接对英寸与 dpi 的乘积调用 int()。浮点数未必精确表示 8.2,乘积如果略小于 820,向零取整就得到 819。3.11.1 中实际画布与这一计算使用的尺寸规则不一致,才暴露了差值。
触发输入的结构如下,需要环境已有 Pillow 才能使用该 writer:
from matplotlib import animation
from matplotlib import pyplot as plt
fig, ax = plt.subplots(figsize=(8.2, 7), dpi=100)
line, = ax.plot([], [])
def update(frame):
line.set_data([0, 1], [0, frame])
return line,
movie = animation.FuncAnimation(fig, update, frames=1, blit=False)
# 实际任务另存到新路径,保留已有动画。
# movie.save("checked-animation.gif", writer=animation.PillowWriter(fps=1), dpi=100)
单帧也足以暴露像素行宽问题,不需要先调整动画帧率或 blit。这里保留保存调用的位置,文件名应按应用实际输出选择,避免覆盖已有结果。
3.11.2 的帧尺寸计算已加浮点容差
Matplotlib 3.11.2 正式实现将帧尺寸改为类似 int(width_in_inches * dpi + 1e-8) 的计算,消除接近整数边界时的这一舍入误差。项目允许时,可按依赖锁文件采用包含该处理的正式版本,再用原尺寸和原 writer 检查输出。
暂时不能更新且允许改变画布大小时,可选乘积能稳定得到整数像素的尺寸,例如 8×7 英寸、100 dpi。那会变成 800×700 的画布,属于尺寸调整;不能在必须保持 820 像素宽的交付中悄悄替换。任意改一个小数、直到肉眼不歪,不是稳定的尺寸约定。
也不应直接把原始像素缓冲截短来迎合 819 宽度,这会丢数据,并可能把后续每行边界进一步弄错。更换 writer 会改变输出路径及依赖,也需要独立核对,不能只以文件生成成功判断。
对齐帧宽和原始字节数
诊断时在相同导出 dpi 下核对画布尺寸、writer 的 frame_size 与 RGBA 字节数;RGBA 每像素四字节,普通连续帧应与宽×高×4一致。只看交互窗口 dpi,可能与保存时采用的 dpi 不同。
验收打开最终 GIF,检查边框、坐标轴和原本应竖直的线是否逐行对齐,并在多帧任务中检查每帧尺寸一致。若两处尺寸已经相同而仍变形,应继续查像素格式、动画更新与实际播放,不把所有 GIF 质量问题都归为这项取整差异。


