用Golang写一个365天的四季视频,把时光装进代码里

你有没有想过,如果有一天,你能把一整年的风景压缩成一段视频,会是什么样子?春天第一朵花开,夏天雷雨砸在窗上,秋天落叶打着旋儿落下,冬天雪...

你有没有想过,如果有一天,你能把一整年的风景压缩成一段视频,会是什么样子?春天第一朵花开,夏天雷雨砸在窗上,秋天落叶打着旋儿落下,冬天雪把世界变得安静——所有这些,如果浓缩成几分钟,那该多震撼。

我去年还真试了一把,用一台树莓派,一个摄像头,再加上Golang写的脚本,硬生生拍了一整年,每天固定时间拍一张照片,最后合成一段视频,过程磕磕绊绊,但结果让我愣了好久,今天就把这整个过程拆开揉碎了讲给你听,从硬件选型到代码实现,再到视频合成,你照着做也能搞一个。

为什么选Golang而不是Python?

你可能会想,Python不是更适合这种任务吗?有现成的opencv库,有picamera的绑定,写起来更简单,我一开始也是用的Python,但跑了一周就崩了三次——内存泄漏、线程卡死、莫名其妙的中文编码问题,后来一气之下换成了Golang,跑了整整365天,一次没崩。

Golang的并发模型太适合这种长期运行的任务了,想象一下,你的程序需要:

  • 每天定时拍照
  • 把图片保存到硬盘
  • 偶尔检查磁盘空间
  • 记录日志方便排错
  • 可能还要同步到云端

在Python里你需要小心翼翼地管理线程、处理异常,在Go里呢?一个goroutine搞定一个任务,用channel传数据,整个程序结构清晰得像搭积木,而且Go编译出来是单文件二进制,丢到树莓派上就能跑,不用装依赖,不用管Python版本。

硬件准备:一台相机,一块树莓派,一颗耐心

我先列一下我用的装备,不一定非得一模一样,原理通了就好:

组件 型号 用途
单板电脑 Raspberry Pi 4B (4GB) 核心控制
摄像头 Raspberry Pi Camera Module v2 8百万像素,够用
存储 64GB MicroSD + 1TB 外接硬盘 每天存照片
外壳 金属防水盒 扔在阳台
电源 树莓派官方电源 跑了整年

总成本大概一千出头,比买一台延时相机便宜多了,而且可控性高到离谱——你能控制快门、ISO、白平衡,甚至能实时修改计划任务。

代码实现:从每天的定时器说起

核心逻辑其实就三件事:定点拍照、存为JPEG、把图片串成视频,看起来简单,但细节抠起来能写满一张A4纸。

第一步:用Cron的思想写Go定时器

树莓派的GPIO可以直接触发摄像头,但我不想让它24小时跑着,用Golang的time.Ticker配合cron表达式,精确到分钟级就够了:

package main
import (
    "fmt"
    "time"
    "github.com/robfig/cron/v3"
)
func main() {
    c := cron.New()
    // 每天早上8点和傍晚18点拍照
    c.AddFunc("0 8,18 * * *", func() {
        capturePhoto()
    })
    c.Start()
    // 让程序一直跑
    select {}
}

这里我选了每天拍两次——早晨的柔和光线和傍晚的黄金时刻,你要是想拍出四季的更迭,其实一天一次也够,但两次能捕捉到更多光影变化,夏天早晨5点天就亮了,冬天8点可能还黑着,所以后来我改成了根据日出日落时间动态调整——这个后面细说。

第二步:拍照这件事,比你想的麻烦

调用系统命令raspistill是最直接的方式,但得处理错误、命名文件、检查目录是否存在:

import "os/exec"
func capturePhoto() {
    dateStr := time.Now().Format("2006-01-02_150405")
    fileName := fmt.Sprintf("/photos/%s.jpg", dateStr)
    cmd := exec.Command("raspistill", 
        "-o", fileName,
        "-w", "1920",  // 宽度
        "-h", "1080",  // 高度  
        "-q", "90",    // 质量
        "-t", "1",     // 延时1毫秒
        "-ex", "auto", // 自动曝光
    )
    err := cmd.Run()
    if err != nil {
        log.Printf("拍照失败: %v", err)
        // 这里可以发个告警邮件
    }
}

嗯,看起来很简单对吧?但坑就藏在细节里,冬天的早晨,摄像头玻璃上会结一层霜,拍出来的照片白茫茫一片,我加了一段检测代码——计算图片的平均亮度,如果低于某个阈值就重拍一次:

import "image/jpeg"
func checkBrightness(filePath string) bool {
    file, _ := os.Open(filePath)
    img, _ := jpeg.Decode(file)
    bounds := img.Bounds()
    var totalBrightness int64
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            r, g, b, _ := img.At(x, y).RGBA()
            brightness := (r + g + b) / 3
            totalBrightness += int64(brightness >> 8)
        }
    }
    avg := totalBrightness / int64(bounds.Dx() * bounds.Dy())
    return avg > 30  // 阈值经验值
}

这个办法挺笨的,但管用,后来我还加了去雾算法,直接用OpenCV处理——不过那又是另一个故事了。

用Golang写一个365天的四季视频,把时光装进代码里

视频合成:Golang也能干FFmpeg的活

照片攒够365天后,得把它们合成视频,如果每张照片播放0.1秒,365张就是36.5秒,但你想表现出四季的节奏——春天可以慢一点,冬天落叶的瞬间可以快一点,所以帧率不能固定。

这时候Golang的os/exec再次出场,调用FFmpeg来合成,但FFmpeg参数多,容易写错,我封装了一个函数:

func createVideo(photoDir string, outputName string) {
    // 先生成一个临时文件列表
    listFile := "/tmp/photo_list.txt"
    file, _ := os.Create(listFile)
    files, _ := ioutil.ReadDir(photoDir)
    sort.Slice(files, func(i, j int) bool {
        return files[i].Name() < files[j].Name()
    })
    for _, f := range files {
        file.WriteString(fmt.Sprintf("file '%s/%s'\n", photoDir, f.Name()))
    }
    file.Close()
    cmd := exec.Command("ffmpeg",
        "-f", "concat",
        "-safe", "0",
        "-i", listFile,
        "-vf", "fps=10,scale=1920:1080",  // 10帧/秒,一天算一帧
        "-c:v", "libx264",
        "-preset", "medium",
        "-crf", "23",
        outputName,
    )
    cmd.Run()
}

参数调了几十次才发现,-crf 23是个不错的平衡点——文件不大,画质还能接受,你要是想要更清晰,可以用18,但文件会大三倍。

四季视频的魔法:从24小时到365天

真正让这段视频有魔力的,不是技术,而是时间的选择,我花了两个月才摸索出最优的拍照时间表:

季节 拍照时间 原因
春天 6:30, 18:30 光线柔和,植物颜色饱和
夏天 5:00, 19:30 避开正午死白
秋天 7:00, 17:00 黄昏光线拉长影子
冬天 8:30, 16:00 日照短,抓住金色时刻

我写了一个算法,根据经纬度自动计算日出日落时间,用的是astronomy库里的公式,精度能到几分钟,这样程序就能自动调整拍照时间——太阳刚升起时拍一张,落日前再拍一张。

func calcSunrise(lat, lng float64, date time.Time) time.Time {
    // 这里用了天文算法,不是简单的查表
    // 输入北京坐标:39.9°N, 116.4°E
    // 输出精确到分钟
}

后期处理:让流畅度翻倍

单纯的逐帧播放看起来很卡,因为每天的光照条件不同,相邻两张照片的亮度可能差很大,我用了渐变过渡——在两张照片之间插值生成中间帧,Golang没有现成的库,我手动实现了线性插值:

func blendImages(img1, img2 image.Image, ratio float64) *image.RGBA {
    bounds := img1.Bounds()
    result := image.NewRGBA(bounds)
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            r1, g1, b1, a1 := img1.At(x, y).RGBA()
            r2, g2, b2, a2 := img2.At(x, y).RGBA()
            r := uint8(float64(r1>>8)*(1-ratio) + float64(r2>>8)*ratio)
            g := uint8(float64(g1>>8)*(1-ratio) + float64(g2>>8)*ratio)
            b := uint8(float64(b1>>8)*(1-ratio) + float64(b2>>8)*ratio)
            result.Set(x, y, color.RGBA{r, g, b, 255})
        }
    }
    return result
}

这代码跑起来特别慢——处理一张照片就要几秒钟,但效果真的值得,原本硬切换看起来像幻灯片,加了渐变后就像流淌的时光。

避坑指南:那些让我抓狂的细节

这一年里翻车无数,挑几个有代表性的:

  1. 梅雨季镜头起雾:用塑料袋套了个硅胶干燥剂,丑是丑了点,但有效
  2. 夏天过热树莓派死机:加了个小风扇,用GPIO控制温控
  3. 存储空间突然爆满:写了个监控程序,每检查一次,如果剩余空间<10%就自动压缩老照片
  4. 偶尔拍出全黑照片:原因是云层太厚,自动曝光补偿不够,后来加了个重试机制,拍3次取最亮的一张
  5. 时间跳跃:树莓派没联网时时钟会重置,用了NTP同步后解决

每个问题都对应着一小段代码——Golang的错误处理在这里太实用了。defer保证文件一定能关闭,panic能在真正出问题时及时重启。

最终效果:一段4分12秒的视频

365天,每天2张照片,总共730张初始素材,去掉因为各种原因废掉的大概30张,实际用了700张,合成后的视频全长4分12秒,刚好是一首歌的长度。

前几天把它放给朋友看,一开始屏幕上是冬天的枯枝,雪慢慢变小;然后画面突然亮了——春天第一抹绿色冒出来,速度比想象中快;夏天那段树叶疯长,光影在午后拉得很长;秋天最让人震撼——叶子从绿变黄再变红,只用了几秒钟,像有人打翻了调色盘。

朋友说看得想哭。

这就是时间的格式,而Golang帮我把它存成了4分12秒。

本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://hljbesthome.com/fnagchan/1886.html

(18)

文章推荐

发表回复

本站作者才能评论

评论列表(4条)

  • kyadmin
    kyadmin 2026-07-29

    我是be365的签约作者“kyadmin”!

  • kyadmin
    kyadmin 2026-07-29

    希望本篇文章《用Golang写一个365天的四季视频,把时光装进代码里》能对你有所帮助!

  • kyadmin
    kyadmin 2026-07-29

    本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名

  • kyadmin
    kyadmin 2026-07-29

    本文概览:你有没有想过,如果有一天,你能把一整年的风景压缩成一段视频,会是什么样子?春天第一朵花开,夏天雷雨砸在窗上,秋天落叶打着旋儿落下,冬天雪...

    联系我们

    工作时间:周一至周五,9:30-18:30,节假日休息

    关注我们