项目场景:
app里帖子展示时会首先加载一个图片/视频的缩略图,在高清图片/视频加载完毕,再去替换。目前线上视频缩率图模糊。所以本次项目希望提升视频缩略图清晰度,同时借此机会对图片大小进行压缩,降低带宽成本,减少用户流量损耗。
1、图片jpg转webp格式(结论:平均图片大小压缩率能达到50%)
2、使用ffmpeg选取前2秒60帧挑优作为封面(如果希望减少缩略图和视频起始的差距,可以选取0.5秒内的帧)
环境准备:
1、opencv 4.5.4 用于计算图片的相关指标(查看版本pkg-config --modversion opencv
)
2、ffmpeg
3、imagemagick 7.0.10-23 用户图片格式转换,需要支持webp、png、jpg以及xml(支持http图片url)。
Delegates (built-in): bzlib fontconfig freetype jng jpeg lzma png webp x xml zlib
5、go 1.16.6
4、gocv 0.28.0
名词:
1、Laplacian 拉普拉斯矩阵,图论。
图片压缩格式优化
- webp:webp为网络图片提供了无损和有损压缩能力,同时在有损条件下支持透明通道。据官方实验显示:无损WebP相比PNG减少26%大小;有损WebP在相同的SSIM(Structural Similarity Index,结构相似性)下相比JPEG减少25%~34%的大小;有损WebP也支持透明通道,大小通常约为对应PNG的1/3。
- heif:HVEC或H.265视频格式的静态图像版本。HEIF图像比JPEG格式的相同图片小50%。
- avif:下一代技术。一种来源于AV1的图像格式,其中AV1是一种视频压缩技术。具有较高的压缩能力,相比于JPEG格式有着10倍以上的进步。
通过视频抽帧获取无损图片(png格式)。转码有损格式:jpeg、webp、heif、avif。在尽量少损害图片质量的前提下,尽量减低图片大小。通过比对DSSIM,获取最佳质量数值。DSSIM值越小,越趋近原图。
实验一:相同quality,对比图片大小
格式 | 工具 | quality | 大小占比 | DSSIM |
---|---|---|---|---|
jpeg | imagemagick | 70 | 19.18% | 0.0055 |
webp | imagemagick | 70 | 10.24% | 0.0072 |
heif | imagemagick | 70 | 15.17% | 0.0031 |
avif | imagemagick | 70 | 6.20% | 0.0075 |
结论:quality相同,不同格式的图片质量不同。
实验二:获取合适quality
格式 | 工具 | quality | 大小占比 | DSSIM |
---|---|---|---|---|
ffmepg_jpeg | ffmpeg | 2 [2-31] | 25.92% | 0.0055 |
jpeg | imagemagick | 70 [0-100] | 19.18% | 0.0072 |
webp | imagemagick | 70 [0-100] | 10.24% | 0.0078 |
heif | imagemagick | 40 [0-100] | 684.% | 0.0071 |
avif | imagemagick | 60 [0-100] | 5.59% | 0.0080 |
magick a.png -quality 70 a.jpg
magick url图片 -quality 70 a.webp
视频缩略图优化
印尼用户上传视频质量不佳,原始抽取视频首帧作为视频缩略图方案存在瓶颈,存在首帧模糊、黑屏等问题,导致缩略图质量差,不能正向引导用户查看视频。
方案:
使用无参考图像清晰度评估方案,对视频2s内图像序列(取前50帧图像)基于Laplacian梯度函数做图像清晰度预估。
Golang实现
package main
import (
"errors"
"fmt"
"gocv.io/x/gocv"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
var TmpFileDir = "test"
func main() {
fmt.Println("shotframev2")
ShotFrameV2("https://tenfei01.cfp.cn/creative/vcg/800/version21/VCG21gic20080842.jpg")
}
func ShotFrameV2(desFile string) (frameFilepath string, err error) {
tmpFile := TmpFileDir
fpre := fmt.Sprintf("%d-framev2", 1)
outFilepath := filepath.Join(tmpFile, fmt.Sprintf("%s-%s.png", fpre, "%d"))
frameFilepaths := filepath.Join(tmpFile, fmt.Sprintf("%s-%s.png", fpre, "*"))
frames := 60
// 前2s取60帧
args := []string{
"-i", desFile,
"-threads", "1",
"-ss", "00:00:00.000", // 开始位置
"-t", "2", // 截取视频长
"-vframes", strconv.FormatInt(int64(frames), 10), // 取60帧
"-f", "image2",
outFilepath,
}
cmd := exec.Command("ffmpeg", args...)
err = cmd.Run()
if err != nil {
fmt.Printf("failed to ShotFrameV2, cmd: %s, err: %s\n", strings.Join(args, " "), err.Error())
return
}
preFrame := ""
indexLapMap := make(map[int]float64) // 每个下表的lap值
diffIndexs := make([]int, 0) //每个不同的第一个diff下标
for i := 1; i <= frames; i++ {
frame := filepath.Join(tmpFile, fmt.Sprintf("%s-%d.png", fpre, i))
if has, _ := PathExists(frame); !has {
break
}
if i == 1