goodcharacters 动图替换背景

创建:xiaozi · 最后修改:xiaozi 2020-01-04 19:27 ·

背景目标

goodcharacters 这个网站提供了文字书写顺序的动图,但动图上有些水印标识,处于文字下方的背景图,看着总是怪别扭的,所以尝试着把背景给换了。

简单展示

原图是这个样子的:

朕

处理之后是这个样子的:

朕 朕

原理分析

gif 图片是由一帧一帧的图片组成的动画,所以我们需要对 gif 进行一帧一帧的拆解,把每一帧的背景图去掉之后再加上新的背景图,然后再重新组装成一张 gif 图片。

frames

零、安装依赖

整个处理的过程,我们需要用到 Python 的图片处理库:Pillow,使用 pip 直接安装最新版 7.0.0。

pip install Pillow

一、获取背景图

比较幸运的是 gif 的第一帧就提供了一张完整的背景图,这样就不需要手动去拼凑一张背景图,直接从 gif 中提取出第一帧就可以了。

二、拆解 gif

按照 Pillow 的官方文档将 gif 图片先拆成 png 看一下

input_image = Image.open(input_name)
for index in range(0, input_image.n_frames):
    input_image.seek(index)
    frame = input_image.convert("RGBA")
    frame.save("./imgs/frame%d.png" % index)

incorrect frames WTF! 咋回事?笔画的颜色不对,还多了很多方块;Pillow 对 gif 的读取存在 bug,需要先打个补丁。

from PIL import Image, ImageFile
from PIL.GifImagePlugin import GifImageFile, _accept, _save, _save_all

# workaround initialization code
class AnimatedGifImageFile(GifImageFile):

    def load_end(self):
        ImageFile.ImageFile.load_end(self)


Image.register_open(AnimatedGifImageFile.format, AnimatedGifImageFile, _accept)
Image.register_save(AnimatedGifImageFile.format, _save)
Image.register_save_all(AnimatedGifImageFile.format, _save_all)
Image.register_extension(AnimatedGifImageFile.format, ".gif")
Image.register_mime(AnimatedGifImageFile.format, "image/gif")
# end of workaround initialization code
prev_frame = None
for index in range(0, input_image.n_frames):
    input_image.seek(index)
    frame = input_image.convert("RGBA")
    if prev_frame and input_image.disposal_method == 1:
        updated = frame.crop(input_image.dispose_extent)
        prev_frame.paste(updated, input_image.dispose_extent, updated)
        frame = prev_frame
    else:
        prev_frame = frame.copy()
    frame.save("./imgs/frame%d.png" % index)

这个时候再看一下,png 图片都正确了。

三、抹去背景图

经过第二步的处理,我们现在已经有了每一帧的图片,以及它的背景图片,接下来就是要删除每一帧图片中的背景图片。

origin简称o + watermark 简称 w

得到 target 简称 t

对于颜色叠加的算法没有了解过的可以详细的看下这篇文章,iconfinder 图标去水印;这里拿已经得到的公式做一下推导。

$$\alpha_t = 1 - (1 - \alpha_o)(1 - \alpha_w)$$ $$\alpha_w = \frac{\alpha_t - \alpha_o}{1 - \alpha_o}$$

$$R_t\alpha_t = R_o\alpha_o(1 - \alpha_w) + R_w\alpha_w$$

问题点:

  1. 如果背景底色是完全不透明的,是没有办法求出字的不透明度的(字有可能是不透明的颜色覆盖到背景上去的,也有可能是半透明的颜色和背景的颜色叠加起来的)

基于问题1,公式中就出现了2个变量,现在不透明度是求不出来了,是不是可以根据公式2来求出字的不透明度,好在这个例子中字的颜色是固定的,拿 Photoshop 给文字取色

font color

$$\alpha_w = \frac{R_o\alpha_o - R_t\alpha_t}{R_o\alpha_o - R_w}$$

这样对图上的像素点进行遍历,然后求出没有背景图的字图

def calc_pixel(input_rgba, background_rgba):
    if background_rgba == input_rgba:
        return 0, 0, 0, 0

    background_red, background_green, background_blue, background_alpha = background_rgba
    input_red, input_green, input_blue, input_alpha = input_rgba

    output_alpha = None
    if background_alpha < 255:
        output_alpha = 255 * (input_alpha - background_alpha) / (255 - background_alpha)

    if output_alpha == 0:
        return 0, 0, 0, 0

    if output_alpha is None:
        output_red = 4
        output_green = 2
        output_blue = 4
        output_alpha = 255 * (background_red * background_alpha - input_red * input_alpha) / (background_red * background_alpha - output_red * 255)
        return output_red, output_green, output_blue, round(output_alpha)

    output_red = (input_red * input_alpha - 255 * background_red * background_alpha) / output_alpha + background_red * background_alpha
    output_green = (input_green * input_alpha - 255 * background_green * background_alpha) / output_alpha + background_green * background_alpha
    output_blue = (input_blue * input_alpha - 255 * background_blue * background_alpha) / output_alpha + background_blue * background_alpha
    return round(output_red), round(output_green), round(output_blue), round(output_alpha)

四、添加新的背景图

在添加新的背景图之前,我们用Photoshop 做两个新的背景图出来,作为演示

green background red background

用 Pillow 可以很方便实现背景图和文字图的合并

new_background_image = Image.open(new_background_name)
new_frame = new_background_image.copy()
new_frame = Image.composite(output_frame, new_frame, output_frame)

五、重新组合成 gif

output_image = output_frames[0]
output_image.save(output_name, save_all=True, optimize=True, append_images=output_frames[1:], duration=frames_duration, loop=0)

最终代码

from PIL import Image, ImageFile
from PIL.GifImagePlugin import GifImageFile, _accept, _save, _save_all

import argparse
import os


# workaround initialization code
class AnimatedGifImageFile(GifImageFile):

    def load_end(self):
        ImageFile.ImageFile.load_end(self)


Image.register_open(AnimatedGifImageFile.format, AnimatedGifImageFile, _accept)
Image.register_save(AnimatedGifImageFile.format, _save)
Image.register_save_all(AnimatedGifImageFile.format, _save_all)
Image.register_extension(AnimatedGifImageFile.format, ".gif")
Image.register_mime(AnimatedGifImageFile.format, "image/gif")
# end of workaround initialization code


def calc_pixel(input_rgba, background_rgba):
    if background_rgba == input_rgba:
        return 0, 0, 0, 0

    background_red, background_green, background_blue, background_alpha = background_rgba
    input_red, input_green, input_blue, input_alpha = input_rgba

    output_alpha = None
    if background_alpha < 255:
        output_alpha = 255 * (input_alpha - background_alpha) / (255 - background_alpha)

    if output_alpha == 0:
        return 0, 0, 0, 0

    if output_alpha is None:
        output_red = 4
        output_green = 2
        output_blue = 4
        output_alpha = 255 * (background_red * background_alpha - input_red * input_alpha) / (background_red * background_alpha - output_red * 255)
        return output_red, output_green, output_blue, round(output_alpha)

    output_red = (input_red * input_alpha - 255 * background_red * background_alpha) / output_alpha + background_red * background_alpha
    output_green = (input_green * input_alpha - 255 * background_green * background_alpha) / output_alpha + background_green * background_alpha
    output_blue = (input_blue * input_alpha - 255 * background_blue * background_alpha) / output_alpha + background_blue * background_alpha
    return round(output_red), round(output_green), round(output_blue), round(output_alpha)

def attach_background(output_frame, new_background_image):
    new_frame = new_background_image.copy()
    new_frame = Image.composite(output_frame, new_frame, output_frame)
    new_frame = new_frame.convert('P')
    return new_frame

def replace_background(input_name, background_name, new_background_name, output_name):
    if not os.path.exists(input_name):
        print("图片不存在")
        return
    input_image = Image.open(input_name)
    background_image = Image.open(background_name)
    new_background_image = Image.open(new_background_name)

    prev_frame = None
    output_frames = []
    frames_duration = []
    for index in range(0, input_image.n_frames):
        input_image.seek(index)
        frame_info = input_image.info
        frame = input_image.convert("RGBA")
        if prev_frame and input_image.disposal_method == 1:
            updated = frame.crop(input_image.dispose_extent)
            prev_frame.paste(updated, input_image.dispose_extent, updated)
            frame = prev_frame
        else:
            prev_frame = frame.copy()

        output_frame = Image.new(frame.mode, frame.size)
        w, h = frame.size
        for x in range(w):
            for y in range(h):
                background_rgba = background_image.getpixel((x, y))
                input_rgba = frame.getpixel((x, y))
                output_rgba = calc_pixel(input_rgba, background_rgba)
                output_frame.putpixel((x, y), output_rgba)

        new_frame = attach_background(output_frame, new_background_image)
        frames_duration.append(frame_info["duration"])
        output_frames.append(new_frame)

    output_image = output_frames[0]
    output_image.save(output_name, save_all=True, optimize=True, append_images=output_frames[1:], duration=frames_duration, loop=0)


def output_path_or_default(input_path, output_path):
    if output_path:
        return output_path
    root, ext = os.path.splitext(input_path)
    return root + ".clean" + ext

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description = "watermark remover for iconfiner")
    parser.add_argument("--output", dest = "output", help = "output image")
    parser.add_argument("input", help = "input image")
    args = parser.parse_args()

    output = output_path_or_default(args.input, args.output)
    replace_background(args.input, "background.png", "background_red.png", output)

浏览 8358 次

Home - Wiki
Copyright © 2011-2024 iteam. Current version is 2.139.0. UTC+08:00, 2024-12-22 23:12
浙ICP备14020137号-1 $Map of visitor$