去年年底,我翻手机相册,发现零零散散拍了一堆碎片,但根本拼不出“这一年到底干了啥”的完整画面,后来看到有人用Python做每日一帧的年度视频,很心动,但Python环境配到我崩溃,刚好那阵子在学Go,就琢磨着:要不……用Go硬啃一个?
现在回头想,那个决定挺“莽”的,但Go语言的并发模型和标准库的简洁性,确实让这个项目变得比想象中可行,这篇文章,我就把整个思路和踩过的坑,像跟朋友唠嗑一样写出来,你不需要是Go高手,只要能跑通go run就行。
为什么是Go,而不是Python或FFmpeg脚本?
说实话,纯用FFmpeg命令行也能把图片拼成视频,但一旦涉及“每个视频片段要加不同的字幕”“某天需要特殊转场”“某些天想跳过(比如没拍素材)”,纯命令行脚本就会变成一团乱麻。
Go的优势在于:
- 编译成单个二进制:部署到NAS或者云服务器上,扔进去就能跑,不用配环境。
- 并发处理图片:比如用
goroutine同时处理多张图片的缩放和叠加,速度比单线程快很多。 - 标准库
image和image/jpeg、image/png:直接解码、修改像素,不需要额外依赖。
用Go写这个,你等于给自己留了一个“以后想加功能就加功能”的底子——比如从Excel读取每天的状态,或者输出不同分辨率的版本。
整体架构:先别写代码,画个时间线
我把整个需求拆成几个阶段:
| 阶段 | 核心任务 | Go相关包 |
|---|---|---|
| 1 | 读取365个日期的素材清单(图片路径、当日文案) | 标准库encoding/csv或encoding/json |
| 2 | 按日期生成单帧画面(背景图+文字叠加) | image、image/draw、golang.org/x/image/font |
| 3 | 把每一帧编码成视频流 | 第三方库github.com/gen2brain/avconv或者调用FFmpeg |
| 4 | 合并音频(可选) | os/exec调用FFmpeg |
我的实际做法是: “Go负责生成图片序列帧,FFmpeg负责把帧合成视频” ,这个分工最稳,因为FFmpeg在视频编码方面太成熟了,Go没必要重复造轮子。
第一步:定义你的“每天”数据结构
先别管视频,先想清楚“一天”长什么样,我定义了一个结构体,用JSON存储,因为这样可以直接从手机APP或者日历导出里转换。
type DailyClip struct {
Date string `json:"date"` // 格式:2023-01-01
ImagePath string `json:"image_path"`
Text string `json:"text"` // 当天想说的话,第一次跑步”
Skip bool `json:"skip"` // 如果没素材,就跳过这天
}
数据文件year.json大概长这样:
[
{"date": "2023-01-01", "image_path": "./photos/0101.jpg", "text": "元旦,睡到自然醒"},
{"date": "2023-01-02", "image_path": "", "text": "", "skip": true},
{"date": "2023-01-03", "image_path": "./photos/0103.jpg", "text": "加班到十点"}
]
注意,我故意留了Skip字段,因为理想很丰满,现实就是你会漏掉很多天,与其硬凑,不如留白,视频里那一帧就用纯色背景+“无记录”三个字。
第二步:生成单帧图片——费曼技巧的实践
费曼技巧说“如果你不能简单地解释它,你就没真正理解它”,写代码也一样,我最初写的图片叠加文字代码,又臭又长,后来发现核心逻辑就三步:
- 打开背景图,缩放到固定尺寸(比如1920x1080)。
- 在图上画一个半透明遮罩(这样白色文字才看得清)。
- 用
freetype库在指定位置写上日期和文字。
这里有个坑:Go自带的image/draw不支持抗锯齿字体渲染,得用github.com/golang/freetype,安装命令:
go get github.com/golang/freetype
核心代码片段,我贴个简版(别笑,这就是我实际跑通的):
func drawText(img *image.RGBA, text string, x, y int, size float64) {
fontBytes, _ := os.ReadFile("/path/to/font.ttf") // 用思源黑体或者微软雅黑都行
f, _ := freetype.ParseFont(fontBytes)
c := freetype.NewContext()
c.SetDPI(72)
c.SetFont(f)
c.SetFontSize(size)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.NewUniform(color.RGBA{255, 255, 255, 255}))
pt := freetype.Pt(x, y)
_, _ = c.DrawString(text, pt)
}
你看,就这么点东西。但要注意,y坐标是文字的基线,不是左上角,我第一次用的时候所有字都跑到图片外面去了,调了半天才明白。
第三步:并发处理——让365张图不卡死
如果一帧一帧地生成,365张图在普通电脑上可能需要几分钟,Go的并发优势这时候就体现出来了,我用了sync.WaitGroup加一个简单的worker pool。
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 最多同时处理5张
for _, day := range days {
wg.Add(1)
sem <- struct{}{}
go func(d DailyClip) {
defer wg.Done()
defer func() { <-sem }()
// 生成单帧,保存为 ./frames/2023-01-01.jpg
}(day)
}
wg.Wait()
注意, 如果你的图片分辨率很大,并发decode + encode会消耗不少内存,我一开始开了20个并发,直接OOM(内存溢出)了,后来调整到5个才稳定。所以并发数不是越大越好,得看你的机器配置。

第四步:用FFmpeg合成为最终视频
图片帧生成后,视频合成就是水到渠成的事,我直接在Go里调用exec.Command,不用写复杂的avconv绑定。
cmd := exec.Command("ffmpeg",
"-framerate", "30",
"-i", "./frames/%04d.jpg", // 注意命名要按顺序,我特意用%04d
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-vf", "scale=1920:1080",
"output.mp4",
)
stdout, err := cmd.Output()
几个容易翻车的细节:
- 帧命名必须零填充:
jpg、jpg在部分工具里会被识别成“1”和“2”而不是“1-2”,所以强制用%04d,比如jpg。 -pix_fmt yuv420p:不加这个,很多播放器(尤其手机上)会打不开视频。- 如果要加背景音乐:再加一个
-i music.mp3 -shortest参数就行。
第五步:处理“没素材”的那几天
这是我的视频里最真实的部分——365天里我大概只有280天有照片,对于缺的那几天,我生成了纯深灰色背景,中间用浅灰字写着“今日无照片,但生活照常”。
这个设计反而让视频有了节奏感:有照片时的热闹,和空白时的安静,交替出现,我朋友看完说,像是一年的呼吸感。
改进方向:别贪多,先跑通
我最初还想加“每天天气”“每日步数”这些数据,后来发现光整理数据就花了三天。第一版,我强烈建议你只保留:日期、一张图、一句话。 跑通完整流程后,再考虑:
- 用
image/gif加个滤镜效果 - 从
fitbit或苹果健康导出每天步数,叠加在角落 - 用
ffmpeg的xfade滤镜做平滑转场(但那个滤镜对帧率要求高,容易卡顿)
我第二版加了步数显示,但语音识别转字幕那个功能至今没做成,因为准确率太感人。
给新手的几点“过来人”建议
- 别用相对路径,写死绝对路径,比如
/home/me/year_project/photos/,因为你在不同目录执行go run可能会找不到文件。 - 测试时用10天数据,别一上来就365天,我调试转场效果时,每次跑365帧,一调就是半小时,后来改成抽前10帧做循环测试。
- 字体文件要单独放。
freetype需要.ttf文件,Linux下可以用/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf,Windows下找C:\Windows\Fonts\msyh.ttc(不过.ttc格式不一定支持,最好转成.ttf)。 - 视频编码很耗CPU,我的老笔记本编码5分钟视频,风扇转得像飞机起飞,如果你机器不行,可以降低分辨率为1280x720,或者用
-preset ultrafast。
最后的最后:跑通了,然后呢?
当你的output.mp4第一次成功播放,看到1月1日的自己笑哈哈地出现在屏幕上,那个感觉……挺奇妙的,你会觉得,原来用代码记录时间,比单纯拍照多了一层“固执”的浪漫。
我自己把这个视频发给了几个朋友,他们问我要代码,其实代码烂得不行,完全没做异常处理,panic随时可能发生,但这就是真实的过程——先让它跑起来,再谈优雅。
如果你也写了一个版本,欢迎在评论区告诉我你的进度,或者卡在哪个环节了,也可以喊我,毕竟,一年365天,哪天都值得被记住。
(注:文中使用的开源库有github.com/golang/freetype、FFmpeg,以及标准库image、os/exec,感谢它们的作者。)
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/tiyu/2580.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Go语言写一个第一天到第365天视频生成器,我的年度记录流水账》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:去年年底,我翻手机相册,发现零零散散拍了一堆碎片,但根本拼不出“这一年到底干了啥”的完整画面,后来看到有人用Python做每日一帧的年度...