我记得去年这时候,我媳妇儿跟我说:“咱们结婚一周年,能不能搞个视频?”我当时就懵了,不是不想搞,是真不知道咋搞,后来我琢磨了一下,既然是搞技术的,那就用代码搞定呗,用Golang写个一周年纪念视频生成工具,365秒,一秒不多一秒不少。
为什么是365秒,为什么用Golang
一周年嘛,365天,365秒,每一秒代表一天,这么一想还挺浪漫的,当然你也可以改成366,闰年嘛,随你。
至于为什么用Golang,实话实说,因为我只会这个,但用下来发现,Go做视频处理其实挺顺手的。FFmpeg在Go里有绑定库,图像处理有imaging,再加个ffmpeg-go,基本上能搞定大部分需求,你要是问我用Python行不行,那肯定行,但Go部署简单,编译出来一个二进制文件扔哪都能跑,省心。
准备工作:装东西
先说说你需要装什么,别怕,不难。

- FFmpeg:视频处理的核心,必须装,Mac用户brew install ffmpeg,Ubuntu用户sudo apt install ffmpeg
- Go语言环境:1.16以上版本就行
- 几个Go库:
go get github.com/u2takey/ffmpeg-go
go get github.com/disintegration/imaging
装完就差不多了,我一开始还装了一堆别的库,后来发现根本用不上。
第一步:准备素材
这个步骤最花时间,但也是最有意思的部分,你得准备365张照片,每张代表一天,别怕,不用真的每天拍一张,可以从手机里挑,从相册里翻,我媳妇儿跟我说“你去年每一天都在干嘛”,我说我哪记得,但照片里有。
照片不用太大,1024x768就够,多了也没用,视频分辨率一般也就1920x1080,你要是照片尺寸不一样,程序会自动缩放,但最好统一一下尺寸,免得变形。
第二步:写代码核心逻辑
好了,说到重点了,代码其实不复杂,但有几个坑我得跟你说说。
package main
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/jpeg"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/disintegration/imaging"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
这一段是库引用,没啥好说的,就是标准的Go写法。
核心逻辑是这样的:读取365张图片,每张图片加个日期标签,然后合成一个视频,视频时长365秒,每张图片显示1秒。
等等,我是不是漏了什么?对了,还要加文字,一周年纪念视频嘛,得有点仪式感,每张图片左下角加个“第X天”,右上角加个日期,中间可以加一句想说的话,但别每张都加,太假了,365张都加一样的话也没意思,我是在开头10张和结尾10张加了话,中间就加个日期。
func addLabel(img image.Image, x, y int, label string) image.Image {
// 这个函数是为了给图片加文字
// 但Go标准库没有直接的文字绘制功能
// 我用的方法是先生成一个带文字的图层,然后叠加
dst := imaging.Clone(img)
// 这里用imaging的DrawText功能
// 实际上imaging也没有DrawText,得用freetype
// 所以我换了个思路
return dst
}
这段代码我是写到一半才发现imaging不支持文字绘制,坑啊,后来我改成了用freetype库,或者更简单的方法:用FFmpeg的文字叠加功能,在合成视频的时候统一加文字。
说实话,我折腾了三天才搞明白这个,如果你不想折腾文字绘制,直接在图片上用Photoshop或者美图秀秀把文字加上,省事。
第三步:合成视频
合成视频这一步,Go调用FFmpeg来做,这是最爽的部分,因为你不用自己处理编码解码。
func createVideo(imageDir, outputFile string) error {
// 先确保图片按顺序重命名
// 从001.jpg到365.jpg
err := renameImages(imageDir)
if err != nil {
return fmt.Errorf("重命名图片失败: %v", err)
}
// 用FFmpeg合成视频
// 输入帧率设为1,每张图片显示1秒
err = ffmpeg.Input(
filepath.Join(imageDir, "%03d.jpg"),
ffmpeg.KwArgs{"framerate": "1"},
).
Output(outputFile, ffmpeg.KwArgs{
"c:v": "libx264",
"pix_fmt": "yuv420p",
"r": "30",
"t": "365",
}).
OverWriteOutput().
Run()
return err
}
关键参数解释一下:
framerate: 1:每秒1帧,对应每张图片显示1秒c:v: libx264:视频编码格式,H.264,最通用的pix_fmt: yuv420p:像素格式,兼容性最好r: 30:输出视频帧率30fps,这样每张图片实际会重复30帧t: 365:视频时长365秒
这步跑完,你就得到一个365秒的视频文件了,但别急着开心,我第一个版本跑出来发现图片顺序不对,因为我的文件名是按照拍照日期来的,不是按照1到365编号的,所以必须在前面加一步重命名。
第四步:重命名图片的小坑
重命名这块,我踩了个坑,Go读取目录文件默认是按文件名排序的,但如果你从手机导出的照片,文件名可能是IMG_20230101_123456.jpg这种,按字母排序会乱。
解决办法:读取所有文件的修改时间,按时间排序后再重命名。
func renameImages(dir string) error {
files, err := os.ReadDir(dir)
if err != nil {
return err
}
type fileInfo struct {
name string
modTime time.Time
}
var fileList []fileInfo
for _, f := range files {
if f.IsDir() {
continue
}
info, _ := f.Info()
fileList = append(fileList, fileInfo{
name: f.Name(),
modTime: info.ModTime(),
})
}
// 按修改时间排序
sort.Slice(fileList, func(i, j int) bool {
return fileList[i].modTime.Before(fileList[j].modTime)
})
// 重命名
for i, f := range fileList {
ext := filepath.Ext(f.name)
newName := fmt.Sprintf("%03d%s", i+1, ext)
os.Rename(
filepath.Join(dir, f.name),
filepath.Join(dir, newName),
)
}
return nil
}
这个逻辑是对的,但有一个问题:如果你的照片跨年了,比如从2023年6月到2024年6月,那按修改时间排序没问题,但如果你的照片是不同设备拍的,修改时间可能不准,稳妥起见,我建议你手动整理一下顺序,把照片按1到365命名好再跑程序。
第五步:加点音乐和转场
光有图片太单调了,我加了背景音乐,选了首我俩都喜欢的歌,剪辑成365秒,用FFmpeg混流就行。
func addMusic(videoFile, musicFile, outputFile string) error {
return ffmpeg.Input(videoFile).
Input(musicFile).
Output(outputFile, ffmpeg.KwArgs{
"c:v": "copy",
"c:a": "aac",
"shortest": nil,
}).
OverWriteOutput().
Run()
}
shortest参数会让输出以最短的流为准,也就是如果音乐比视频长,就自动裁剪。
转场效果我试了试,但发现用Go调用FFmpeg做转场太复杂了,我放弃了,直接在图片之间加了个淡入淡出,FFmpeg的fade滤镜可以搞定。
filter := "fade=t=in:st=0:d=1,fade=t=out:st=364:d=1"
这个滤镜在视频开头和结尾各加1秒的淡入淡出,中间图片切换的转场,我没做,因为365张图片每张1秒,加转场会缩短显示时间,或者需要延长总时长,我最后选择了不要转场,朴素点也挺好。
第六步:处理意外情况
我跑了第一版,发现视频出来了,但有黑屏,排查了半天,发现是有几张图片损坏了,或者格式不对,Go的image库解码时直接panic了。
解决办法:在读取图片时加个错误处理,跳过损坏的图片,用一张默认的灰色图片代替。
func loadImage(path string) image.Image {
img, err := imaging.Open(path)
if err != nil {
// 生成一张灰色图片
img = image.NewRGBA(image.Rect(0, 0, 1024, 768))
draw.Draw(img, img.Bounds(),
&image.Uniform{color.RGBA{128, 128, 128, 255}},
image.Point{}, draw.Src)
}
return img
}
这个处理挺糙的,但管用,灰色的图片在视频里虽然不好看,但总比黑屏强。
第七步:测试和调整
写完了你以为就完了?不不不,调参数调了一天。
第一个问题是视频太大了,365张图片,每张1024x768,编码成H.264,出来一个2GB的文件,太大了,传微信都传不了。
解决办法:降低码率。
"b:v": "2000k", // 视频码率2000kbps
码率降到2000k,文件大小缩减到200MB左右,清晰度还行。
第二个问题是部分图片变形,因为我的照片有的是横着拍的,有的是竖着拍的,竖拍的照片在横屏视频里会留黑边,解决办法:统一填充成16:9比例,不做拉伸。
func resizeToFill(img image.Image, width, height int) image.Image {
return imaging.Fill(img, width, height, imaging.Center, imaging.Lanczos)
}
这样竖拍的照片会被裁剪掉上下部分,但至少不会变形,我挑了一些关键的照片手动调整了构图。
第八步:生成最终版本
所有代码写完后,我整了一个完整的脚本,自动化处理,从整理照片到生成最终视频,一行命令搞定。
实际上我最后跑了三次才成功,第一次是图片顺序错了,第二次是音乐长度不对,第三次才完美,我媳妇儿看完哭了,说“你还挺用心的”,值了。
完整代码我放在GitHub上了,不过你要是自己写一遍,踩一遍坑,最后出来的效果会更有意义,一周年纪念嘛,重要的不是视频多完美,而是那份心意,365秒,记录365天,每一秒都是真实的生活,有点糊的照片,有点歪的构图,反而更真实。
最后成品我放到了家庭群,我妈看了说“这啥玩意儿,怎么这么多照片”,我爸说“人家这是高科技”,好吧,管他呢,反正我媳妇儿喜欢就行。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.hljbesthome.com/lvyou/486.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《用Golang写一周年纪念视频,365秒刚刚好》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:我记得去年这时候,我媳妇儿跟我说:“咱们结婚一周年,能不能搞个视频?”我当时就懵了,不是不想搞,是真不知道咋搞,后来我琢磨了一下,既然是...