goodcharacters 动图替换背景
背景目标
goodcharacters 这个网站提供了文字书写顺序的动图,但动图上有些水印标识,处于文字下方的背景图,看着总是怪别扭的,所以尝试着把背景给换了。
简单展示
原图是这个样子的:
处理之后是这个样子的:
原理分析
gif 图片是由一帧一帧的图片组成的动画,所以我们需要对 gif 进行一帧一帧的拆解,把每一帧的背景图去掉之后再加上新的背景图,然后再重新组装成一张 gif 图片。
零、安装依赖
整个处理的过程,我们需要用到 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)
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 图片都正确了。
三、抹去背景图
经过第二步的处理,我们现在已经有了每一帧的图片,以及它的背景图片,接下来就是要删除每一帧图片中的背景图片。
+
得到
对于颜色叠加的算法没有了解过的可以详细的看下这篇文章,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,公式中就出现了2个变量,现在不透明度是求不出来了,是不是可以根据公式2来求出字的不透明度,好在这个例子中字的颜色是固定的,拿 Photoshop 给文字取色
$$\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 做两个新的背景图出来,作为演示
用 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)
浏览 8211 次