当前位置:首页 > 房产 > 正文

用 Go 语言,把365天压缩成一段会呼吸的影像日记

  • 房产
  • 2026-08-23 21:37:08
  • 45
摘要: 先别急着写代码,想想你为什么要拍这365张照片你有没有过这种时刻——手机里存着几千张照片,却从来没认真回看过?我去年元旦立了个f...

先别急着写代码,想想你为什么要拍这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-012024-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
}

那些坑,我替你先踩过了

  1. 照片方向问题:手机拍的竖图有EXIF旋转信息,但Go的image.Decode默认不处理,你得用golang.org/x/imageexif包或直接读取irotate标签,否则视频里会出现横躺的图片。
  2. 内存暴涨:处理3000x4000的图,每张解码后大概占40MB内存,如果你一口气全部加载再处理,16GB内存也扛不住。务必用流式处理——读一张、处理一张、释放一张。
  3. 时间排序的坑:如果照片文件名不规范,按字典序排序会得到1, 10, 100这种顺序。用Go的sort.Slice配合自定义比较函数,按time.Time排序,而不是字符串排序。
  4. FFmpeg的输入格式:如果你用-framerate 1/3(每3秒一帧),要确保照片文件名是%04d.jpg这种连续编号,建议在Go里先重命名成jpgjpg,省去很多麻烦。

当视频终于生成时,你看到的是什么

我最终生成的视频是4分多钟,每张照片停留3秒,但最后一张(2024年12月31日)停了10秒,背景音乐是鼓点的节拍器,每响一下换一张照片——那种感觉就像把一整年的心跳声录进了影像里

如果你真想玩,还可以加上音效:在Go里用github.com/faiface/beep分析音频的节拍,然后根据拍子决定照片切换时机,或者用gonum做点简单的统计——比如算出你一年里最常拍照的时段,把这些数据叠加成字幕滚动条。

说实话,这个项目代码量不大,我全部下来大概写了600行Go,但最有意思的部分是调试和优化的过程——比如并发数调到多少最合适?我试过8个并发,结果CPU满载导致系统卡顿;改成4个并发加runtime.Gosched(),反而更稳定。

如果你也想试试,别怕写出来的代码不完美,先用最简单的time.Sleep顺序处理,跑通流程,再慢慢加并发、加特效,Go的好处就是它鼓励你先把功能做对,再用优雅的方式做快

最后提一嘴:我的照片很多都是路上随手拍的——地铁里看书的大叔、楼下便利店的值班猫、下雨天窗玻璃上的水痕,这些照片单独看没什么,但串成视频后,你会清晰地看到四季轮换、头发变长、路灯从装饰变成常绿,这种视角只有Godoc能给你,跟语言无关,跟耐心有关。

用 Go 语言,把365天压缩成一段会呼吸的影像日记