引子
一切的起源:学累了描改gif表情包来放松,导进procreate看到帧数100+的时候还不以为然,想着反正要改的部分是静态的,每帧随便移动一下,每下3秒钟,也就300多秒的事!然鹅......
博主不会用AE等软件,也不想下载......
没事没事,没什么事是不能用一行代码解决的!如果不行,那就两行代码
注意:仅使用于替换处和替换图片都不变的改图
-
输入:一张动图(
cat.gif
),一张透明背景的图像(BB.png
) -
输出:动图中每一帧的部分位置被新图像替换后的新 GIF(
cat_with_avatar.gif
)
不想在这个站点放我的画,就仅展示一下所输入动图的第一帧,有蜘蛛猫预警:
(预警完全无用啊 下次装个可折叠图像的插件)
步骤一:提取 GIF 的第一帧
先从 GIF 中提取第一帧(这步手动也很简单,但代码也很简单嗯嗯),用来观察并进行标注:
from PIL import Image
import os
gif_path = 'cat.gif'
output_dir = 'frames'
os.makedirs(output_dir, exist_ok=True)
# 提取第一帧
with Image.open(gif_path) as im:
im.seek(0) # 第一帧
im.convert("RGBA").save(f"{output_dir}/frame_000.png")
print("保存了第一帧为 frame_000.png")
步骤二:手动标记替换位置的圆心和轮廓
(因为这里要替换的图像是圆形的,所以用圆形追踪,别的形状也可以视作圆形来近似处理)
为了自动追踪替换位置,我们需要用鼠标点击:
- 第一个点 是替换位置的中心;
- 第二个点 是替换边缘轮廓上的一点,用于计算替换图像半径。
import cv2
import matplotlib.pyplot as plt
points = []
def click_event(event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
print(f"点击位置:({x}, {y})")
points.append((x, y))
if len(points) == 2:
cv2.destroyAllWindows()
# 载入第一帧
frame_path = 'frames/frame_000.png'
img = cv2.imread(frame_path)
img_copy = img.copy()
cv2.imshow("点击猫头中心 -> 然后点击轮廓", img)
cv2.setMouseCallback("点击猫头中心 -> 然后点击轮廓", click_event)
cv2.waitKey(0)
# 计算圆心和半径
(center_x, center_y), (edge_x, edge_y) = points
radius = int(((center_x - edge_x)**2 + (center_y - edge_y)**2) ** 0.5)
print(f"猫头圆心:({center_x}, {center_y}), 半径:{radius}")
例如我点击的结果是:
猫头圆心:(239, 44), 半径:38
步骤三:模板匹配 + 自动替换每一帧
我们从第一帧中裁出需要修改的区域作为模板,后续每一帧用 OpenCV 的 matchTemplate 方法匹配相同图像的位置,然后粘贴上我们准备好的透明底图像。
import os
import cv2
from PIL import Image, ImageSequence
import numpy as np
# ==== 路径配置 ====
gif_path = 'cat.gif' # 原始动图
avatar_path = 'BB.png' # 替换头像
output_gif = 'cat_with_avatar.gif' # 输出动图路径
output_frames_folder = 'replaced_frames' # 存储每帧输出图
# ==== 初始点击结果 ====
center_x, center_y = 239, 44 # 坐标,上一步得到的结果
radius = 38 # 半径,上一步得到的结果
template_size = radius * 2
os.makedirs(output_frames_folder, exist_ok=True)
# 1. 打开 GIF 并提取第一帧作为模板
original = Image.open(gif_path)
first_frame = original.copy().convert("RGBA")
first_cv = cv2.cvtColor(np.array(first_frame), cv2.COLOR_RGBA2BGR)
template = first_cv[
center_y - radius : center_y + radius,
center_x - radius : center_x + radius
]
# 2. 加载准备好的图片
avatar = Image.open(avatar_path).convert("RGBA")
avatar_width, avatar_height = avatar.size
# 3. 遍历所有帧,模板匹配并叠加头像
frames = []
durations = []
for i, frame in enumerate(ImageSequence.Iterator(original)):
durations.append(frame.info.get('duration', 40)) # 默认帧间隔40ms
frame_rgba = frame.convert("RGBA")
frame_cv = cv2.cvtColor(np.array(frame_rgba), cv2.COLOR_RGBA2BGR)
# 模板匹配(在整张图里找最相似区域)
result = cv2.matchTemplate(frame_cv, template, cv2.TM_CCOEFF_NORMED)
_, _, _, max_loc = cv2.minMaxLoc(result)
top_left = max_loc
cx = top_left[0] + radius
cy = top_left[1] + radius
# 将头像贴到对应位置(以中心点为锚)
paste_x = cx - avatar_width // 2
paste_y = cy - avatar_height // 2
new_frame = frame_rgba.copy()
new_frame.paste(avatar, (paste_x, paste_y), avatar)
# 保存新帧
new_frame_path = os.path.join(output_frames_folder, f"frame_{i:03d}.png")
new_frame.save(new_frame_path)
frames.append(new_frame)
# 4. 合成新 GIF
frames[0].save(
output_gif,
save_all=True,
append_images=frames[1:],
duration=durations,
loop=0,
disposal=2
)
print(f"生成完成:{output_gif}")
效果展示
依旧和开头一样的原因,没有效果展示~ 有缘的话或许某天能在推上刷到呢
但总之效果很好,水了一张图还水了一篇博客(
总结与发散
总结
在这次偷懒(bushi)中,简单用到了图像处理的几个知识点:
- Pillow 处理 GIF 帧;
- OpenCV 实现模板匹配;
- 鼠标交互 + 自动化图像合成。
更多的思考
1.用 AI 检测代替模板匹配
A. 使用目标检测模型(如 YOLOv5、YOLOv8,之前的多媒体安全小组汇报有尝试过)训练一个检测器
B. 也可以直接使用 MediaPipe / Dlib / OpenCV Haar cascades 进行识别
检测出猫头 bounding box 后,根据中心点叠加头像即可
2.如果目标不仅仅是位置变化,还会角度、大小、轻微旋转或者表情变化
可以使用光流法(Optical Flow)或 关键点跟踪(KLT、ORB、SIFT) 获取每帧的猫头角度、变形
根据这些变化,对头像图像做仿射变换后再贴上:
M = cv2.getRotationMatrix2D(center, angle, scale)
warped_avatar = cv2.warpAffine(avatar_np, M, (w, h))
3.可不可以实现像 TikTok 特效那种“动态表情贴纸”?
检测脸部关键点(类似美颜相机那种点),可以用 Dlib 的 shape_predictor_68_face_landmarks.dat 或 MediaPipe FaceMesh,通过关键点来判断头的方向、嘴巴是否张开、眼睛是否闭合等,然后动态变换头像或局部贴图,让贴图跟着脸动
没了,下次见 也不知道下次什么时候有心再写(shui)这种博客哈哈哈。
好险啊博主差点儿要学会AE了