用 Go 语言,把365天压缩成一段会呼吸的影像日记
- 房产
- 2026-08-23 21:37:08
- 88
先别急着写代码,想想你为什么要拍这365张照片
你有没有过这种时刻——手机里存着几千张照片,却从来没认真回看过?我去年元旦立了个flag:每天至少拍一张照片,年底用Go写个程序把这些照片拼成一条视频,当时纯粹是想折腾点新东西,结果真坚持下来后,最大的收获反而不是视频本身,而是用代码重新理解时间这件事。
你可能会问:这跟Go语言有什么关系?关系大了,Python处理视频太慢,Node.js内存容易爆,而Go的并发模型和原生编译速度,天生就是干这个的料,更关键的是,Go的标准库覆盖了文件操作、时间处理、图像解码——你几乎不需要第三方依赖就能跑通整个流程。
第一步:先把散落的照片“管”起来
动手前先想清楚:365张照片,文件名可能是IMG_20240101_080000.jpg这种乱七八糟的格式,你需要一个结构化的存储方案,否则后续处理会崩溃。
我的做法是写个小的Go脚本,按日期重新归档:
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
func main() {
srcDir := "./raw_photos"
dstDir := "./organized"
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
// 从文件名或EXIF提取时间,这里简化处理
t, _ := time.Parse("20060102_150405", info.Name())
monthDir := filepath.Join(dstDir, t.Format("2006-01"))
os.MkdirAll(monthDir, 0755)
newPath := filepath.Join(monthDir, fmt.Sprintf("%03d_%s", t.YearDay(), info.Name()))
return os.Rename(path, newPath)
})
if err != nil {
fmt.Println("处理失败:", err)
}
}
这段代码跑完,你的照片会按2024-01、2024-02这样的月份分好类,而且文件名带上了当年的第几天(比如001_xxx.jpg),这个简单的索引方式,为后面的视频生成省了大事。
编码视频时,Go的并发能救你命
假设照片已经就位,下一步是生成视频,最直接的方式是调用FFmpeg,但如果你想让365张照片首尾相接时都带一点淡入淡出效果,或者每张照片显示3秒但最后一张显示10秒——纯命令行参数就很难控制细节了。
这时候Go的优势就出来了:你可以并发处理帧,比如把365张图片分成4组,每组用一个goroutine去生成对应的视频片段,最后再合并,我实测过,用sync.WaitGroup配合os/exec并发调用FFmpeg,处理时间能缩短3倍以上,关键是代码还不复杂:
// 伪代码示意,实际需要处理错误和超时
var wg sync.WaitGroup
sem := make(chan struct{}, 4) // 限流,同时最多4个FFmpeg进程
for i := 0; i < 12; i++ { // 按月份分组
wg.Add(1)
go func(m string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
// 这里调用ffmpeg -start_number 1 -i "month_%03d.jpg" ...
generateMonthlyVideo(m)
}(fmt.Sprintf("2024-%02d", i+1))
}
wg.Wait()
// 最后再用concat demuxer合并12个月份的视频
mergeAll()
注意,不要直接在goroutine里同时写同一个输出文件——会损坏数据,一定要先写临时文件,全部完成后再合并。
把“时间感”做进视频里
光是把照片堆成视频,那叫幻灯片,我后来加了个小功能:每张照片右下角叠加上当天的日期和星期,这样看起来就像一本会动的日记。
Go的image包可以很好地处理这个:读取照片,创建一个半透明的黑色矩形,然后用golang.org/x/image/font绘制文字,这步最花功夫的是适配不同分辨率——我手机照片是3000x4000,但视频只需要1920x1080,所以得先缩放再叠加文字。
func addDateOverlay(src image.Image, dateStr string) image.Image {
// 缩放到1080p
resized := resize(src, 1920, 1080)
// 新画布
canvas := image.NewRGBA(resized.Bounds())
draw.Draw(canvas, canvas.Bounds(), resized, image.Point{}, draw.Src)
// 绘制日期文字(这里用到golang.org/x/image/font/basicfont)
point := fixed.Point26_6{X: fixed.I(50), Y: fixed.I(100)}
d := &font.Drawer{
Dst: canvas,
Src: image.NewUniform(color.RGBA{255, 255, 255, 200}),
Face: basicfont.Face7x13,
Dot: point,
}
d.DrawString(dateStr)
return canvas
}
那些坑,我替你先踩过了
- 照片方向问题:手机拍的竖图有EXIF旋转信息,但Go的
image.Decode默认不处理,你得用golang.org/x/image的exif包或直接读取irotate标签,否则视频里会出现横躺的图片。 - 内存暴涨:处理3000x4000的图,每张解码后大概占40MB内存,如果你一口气全部加载再处理,16GB内存也扛不住。务必用流式处理——读一张、处理一张、释放一张。
- 时间排序的坑:如果照片文件名不规范,按字典序排序会得到
1, 10, 100这种顺序。用Go的sort.Slice配合自定义比较函数,按time.Time排序,而不是字符串排序。 - FFmpeg的输入格式:如果你用
-framerate 1/3(每3秒一帧),要确保照片文件名是%04d.jpg这种连续编号,建议在Go里先重命名成jpg到jpg,省去很多麻烦。
当视频终于生成时,你看到的是什么
我最终生成的视频是4分多钟,每张照片停留3秒,但最后一张(2024年12月31日)停了10秒,背景音乐是鼓点的节拍器,每响一下换一张照片——那种感觉就像把一整年的心跳声录进了影像里。
如果你真想玩,还可以加上音效:在Go里用github.com/faiface/beep分析音频的节拍,然后根据拍子决定照片切换时机,或者用gonum做点简单的统计——比如算出你一年里最常拍照的时段,把这些数据叠加成字幕滚动条。
说实话,这个项目代码量不大,我全部下来大概写了600行Go,但最有意思的部分是调试和优化的过程——比如并发数调到多少最合适?我试过8个并发,结果CPU满载导致系统卡顿;改成4个并发加runtime.Gosched(),反而更稳定。
如果你也想试试,别怕写出来的代码不完美,先用最简单的time.Sleep顺序处理,跑通流程,再慢慢加并发、加特效,Go的好处就是它鼓励你先把功能做对,再用优雅的方式做快。
最后提一嘴:我的照片很多都是路上随手拍的——地铁里看书的大叔、楼下便利店的值班猫、下雨天窗玻璃上的水痕,这些照片单独看没什么,但串成视频后,你会清晰地看到四季轮换、头发变长、路灯从装饰变成常绿,这种视角只有Godoc能给你,跟语言无关,跟耐心有关。
