你有没有想过,如果有一天,你能把一整年的风景压缩成一段视频,会是什么样子?春天第一朵花开,夏天雷雨砸在窗上,秋天落叶打着旋儿落下,冬天雪把世界变得安静——所有这些,如果浓缩成几分钟,那该多震撼。
我去年还真试了一把,用一台树莓派,一个摄像头,再加上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也能干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
}
这代码跑起来特别慢——处理一张照片就要几秒钟,但效果真的值得,原本硬切换看起来像幻灯片,加了渐变后就像流淌的时光。
避坑指南:那些让我抓狂的细节
这一年里翻车无数,挑几个有代表性的:
- 梅雨季镜头起雾:用塑料袋套了个硅胶干燥剂,丑是丑了点,但有效
- 夏天过热树莓派死机:加了个小风扇,用GPIO控制温控
- 存储空间突然爆满:写了个监控程序,每检查一次,如果剩余空间<10%就自动压缩老照片
- 偶尔拍出全黑照片:原因是云层太厚,自动曝光补偿不够,后来加了个重试机制,拍3次取最亮的一张
- 时间跳跃:树莓派没联网时时钟会重置,用了NTP同步后解决
每个问题都对应着一小段代码——Golang的错误处理在这里太实用了。defer保证文件一定能关闭,panic能在真正出问题时及时重启。
最终效果:一段4分12秒的视频
365天,每天2张照片,总共730张初始素材,去掉因为各种原因废掉的大概30张,实际用了700张,合成后的视频全长4分12秒,刚好是一首歌的长度。
前几天把它放给朋友看,一开始屏幕上是冬天的枯枝,雪慢慢变小;然后画面突然亮了——春天第一抹绿色冒出来,速度比想象中快;夏天那段树叶疯长,光影在午后拉得很长;秋天最让人震撼——叶子从绿变黄再变红,只用了几秒钟,像有人打翻了调色盘。
朋友说看得想哭。
这就是时间的格式,而Golang帮我把它存成了4分12秒。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://hljbesthome.com/fnagchan/1886.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Golang写一个365天的四季视频,把时光装进代码里》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:你有没有想过,如果有一天,你能把一整年的风景压缩成一段视频,会是什么样子?春天第一朵花开,夏天雷雨砸在窗上,秋天落叶打着旋儿落下,冬天雪...