当前位置:首页 > 编程开发 > 正文

六格切 GIF 复现:一次画完的帧,差值全同号

约 4752 字 · 预计阅读 12 分钟
一句话总结:我按「一次画六格再切」的思路写了 40 行代码,把一张 3×2 的图切成 6 帧合成 GIF。同时用「每帧独立加噪」模拟逐帧生成,对比两组帧的平均色差值:一次画完的差值全同号、标准差 1.17;逐帧生成的差值翻向 2 次、标准差 7.12,其中还有一帧比上一帧更亮,看起来像动画倒回去了一下。

上一篇讲 GPT-Image-2.6 那个 GIF 方案的时候,我说「六帧在同一张画布上,风格天然一致」。

写完自己有点不确定——这话听起来对,但没验证过。

于是动手做了一遍。

一、先写代码:40 行画一张六格图

核心就三件事:画一张 3×2 的大图、按格子裁开、合成 GIF。

from PIL import Image, ImageDraw

W, H = 180, 120          # 每格尺寸
COLS, ROWS = 3, 2
N = COLS * ROWS          # 6 帧
DUR = 120                # 每帧 120 毫秒


def paint(draw, idx, x0, y0, jitter=0):
    """在第 idx 格里画背景渐变 + 一个小球"""
    for y in range(H):
        k = (idx * H + y) / (N * H)
        rgb = (int(238 - 58 * k) + jitter,
               int(244 - 38 * k) + jitter,
               250 - jitter)
        draw.line([(x0, y0 + y), (x0 + W, y0 + y)], fill=rgb)
    cx = x0 + 20 + (W - 40) * idx / (N - 1)      # 球从左往右移
    cy = y0 + H / 2
    r = 16 + jitter // 4
    draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(37, 99, 235))


def build_sheet(jitter_each=False):
    img = Image.new("RGB", (W * COLS, H * ROWS), "white")
    d = ImageDraw.Draw(img)
    for i in range(N):
        c, r = i % COLS, i // COLS
        # 逐帧生成时,每帧有自己的噪声
        j = ([0, 0, 6, -6, 10, 4][i] if jitter_each else 0)
        paint(d, i, c * W, r * H, jitter=j)
    return img


def slice_sheet(img):
    frames = []
    for i in range(N):
        c, r = i % COLS, i // COLS
        frames.append(img.crop((c * W, r * H, (c + 1) * W, (r + 1) * H)))
    return frames

paint 里的 jitter 是关键。正常做法(一次画完)jitter 恒为 0;对比组里我给它加了 [0, 0, 6, -6, 10, 4] 这组偏差,模拟「每帧独立生成时各自的采样噪声」。

合成就一句:

frames[0].save("out.gif", save_all=True, append_images=frames[1:],
               duration=DUR, loop=0)

跑出来:六帧,单帧 180×120,GIF 7.0 KB,循环播放。

先把口径说清楚:这是我用 PIL 画的模拟图,不是真的调 GPT-Image-2.6。 我验证的是「同一张画布切出来的帧」和「各自生成的帧」在数值上的差别,不是那个产品的实际输出。

二、切完之后,六帧是什么样的

取每帧的平均红通道,看它怎么变:

各帧平均红通道:225, 214, 204, 195, 187, 179
逐帧差值:      -11, -10, -9, -8, -8
差值全同号?    是
差值标准差      1.17

背景是从浅到深的渐变,所以六帧的红通道单调下降,每帧降 8 到 11。

重点在「全同号」——五个差值全是负数,方向一致,而且绝对值在稳定收敛(11→10→9→8→8)。

这就是「平滑」在数据上的样子。

三、对比逐帧生成:差值会「翻向」

同一套逻辑,只把 jitter 打开:

各帧平均红通道:225, 214, 209, 191, 194, 182
逐帧差值:      -11, -5, -18, +3, -12
差值全同号?    否,翻向 2 次
差值标准差      7.12

六格切 GIF 复现:一次画完的帧,差值全同号

图 1:上面那组五个柱子全在零线下方,下面那组有一个跑到上面去了

三个问题一次暴露:

第一个,第 4 个间隔是 +3。 第四帧比第三帧还亮。放在动画里,就是「画面突然回弹了一下」——观众看到的是动画顿了一下,然后倒回去。

第二个,差值忽大忽小。 -11、-5、-18、+3、-12,没有任何规律。标准差 7.12,是一次画完那组的 6 倍。

第三个,翻向意味着方向错了。 一次画完的差值全同号,说明整体趋势单一;翻向说明中间有一帧「不听话」。

这三个现象,用肉眼看不出来(单帧看都挺正常),但连起来播就明显了。这就是逐帧生成做 GIF 的老毛病:单张都对,连起来就跳。

四、为什么同一张画布能保证帧间一致

原因不复杂:同一张画布上的六个格子,共享同一套「画图的决定」。

具体说三件事:

光照方向。 一次生成时,模型对整个画面定一个光源方向。六格都按这个方向画阴影。逐帧生成时,每帧独立定,可能出现第三帧光源在左、第四帧在右。

色彩空间。 一次生成用同一套调色,六格色调统一。逐帧生成每帧各自调色,累积起来就偏了。

风格一致性。 线条粗细、笔触、材质,一次生成时是「一口气画完」的,天然一致。

换句话说:它把「一致性」这个难题,转成了「一次画完」这个简单要求。

这里有个可以对照的例子:上下文压缩那篇算过分层压缩的代价——保住全部约束,压缩率只有 36.7%。两者都在做取舍:压缩那边舍的是压缩率,这里舍的是时长和连贯度。

五、这个方法什么时候不管用

三个边界。

一,帧数写死。 六个格子就是六帧。想要更长的动画得画更多格(4×3、5×4),但格子越多,每格分到的分辨率越低。180×120 六格是 540×240 的画布,如果画 4×4 十六格,同样画布下每格只有 135×60。

二,运动是「跳」的不是「滑」的。 六帧之间没有插值,球的位置是一格一格跳过去的。想要顺滑,得靠提高帧率(缩短每帧时长),但帧数不变,加快播放只会让跳得更快。

三,复杂动作画不出来。 我的例子里球只是平移。如果要画「球弹跳、旋转、变形」,六格根本不够表达中间过程。格子数量决定了动作的复杂度上限。

六、坑清单

1. 每格边界要留白,否则切开后内容被切断。

我一开始让球贴着格子边缘移动,切出来第一帧和末帧的球有一半在画面外。改法是给每格留 20px 内边距,球的活动范围限制在里面。

2. 用 getpixel 取平均色,别用 getdata。

Pillow 从某个版本起把 getdata() 标成 deprecated(我这版提示 2027 年移除)。改成 im.resize((1,1)).getpixel((0,0)),效果一样,不报警告。

3. 判断「平滑」要看差值的符号,不是看标准差。

我第一版只输出了标准差,A 和 B 差得不多(15.65 对 14.75),看不出问题。因为背景本身有大跨度渐变,把噪声淹没了。改成算逐帧差值之后,A 的 1.17 和 B 的 7.12 才显出 6 倍差距。

这是个通用的教训:指标选错,结论就看不出来。

4. GIF 的调色板只有 256 色,渐变会出色带。

我的背景是渐变,转成 GIF 之后出现了明显的台阶。要减少色带,可以在保存时传 optimize=False 并手动做抖动,或者干脆用纯色背景。

5. duration 是每帧毫秒数,不是总时长。

想总长一秒,六帧就是 duration=167。写成 duration=1000 会变成每帧一秒、总共六秒。

七、结语

做完这遍,我对那个方案的判断从「听起来聪明」变成「确实有道理」。

原因不是它省了什么,是它把一个难问题换成了简单问题。让六帧保持一致很难,让一张图里的六个格子看起来是一套的,本来就容易。

工程上很多好方案都是这个形状:不去硬解难题,而是换一个问法,让难题消失。

至于这个方法能不能用到别的地方——我觉得判断标准是:你的「序列」能不能塞进一张画布。 能塞,就值得试;塞不进去(比如要画长视频),那就老老实实做逐帧一致性。

参考资料

目录导航
  • 一、先写代码:40 行画一张六格图
  • 二、切完之后,六帧是什么样的
  • 三、对比逐帧生成:差值会「翻向」
  • 四、为什么同一张画布能保证帧间一致
  • 五、这个方法什么时候不管用
  • 六、坑清单
  • 七、结语
    • 参考资料
  • 相关文章:

  • GPT-Image-2.6 的 GIF 不走视频模型:画一张六格图,切出来
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。