一个视频由视频帧构成,每一帧在肉眼可见是一张图片成像
帧的类型:
帧的类型主要参考 视频抽帧处理
电脑中保存的视频文件实际经过了编码压缩,实际上修改视频文件的后缀,实际对视频本身并没有影响,后缀只是封装格式,视频本身压缩的信息是由编码格式决定。
视频原始流 -> 视频编码方式 -> 视频封装格式
也就是说实际抽帧过程,并不是每一帧都是独立的个体,这还取决于关键帧的位置。
笔者这里使用java程序语言实现:
1、导入相关依赖
org.bytedeco
javacv-platform
1.5.7
2、主要思路:获取视频的总帧数,循环遍历,根据入参帧率和视频帧率的数量关系,对满足一定条件的帧进行保存
/**
* 抽帧
* @param frameRate 帧率
* @param storePath 截图图片要存放的路径
* @param filePath 要截图的视频存放路径
*/
public static List<String> grabFrameByFilePath(double frameRate, String storePath, String filePath) throws Exception {
File folder = new File(storePath);
boolean success = true;
if (!folder.exists() && !folder.isDirectory()) {
success = folder.mkdirs();
}
if (!success){
throw new FileNotFoundException("文件夹创建异常");
}
List<String> sourceFiles = new ArrayList<>();
FFmpegFrameGrabber fFmpegFrameGrabber = new FFmpegFrameGrabber(filePath);
fFmpegFrameGrabber.start();
grabFrames(fFmpegFrameGrabber,frameRate,storePath,sourceFiles);
fFmpegFrameGrabber.close();
return sourceFiles;
}
private static void grabFrames(FFmpegFrameGrabber fFmpegFrameGrabber, double frameRate, String storePath, List<String> sourceFiles) throws Exception {
double videoRate = fFmpegFrameGrabber.getFrameRate();
if (frameRate >= videoRate){
//每一帧都获取
doGrabPerFrame(fFmpegFrameGrabber,storePath,sourceFiles);
}else if (frameRate >= ApolloConfig.VIDEO_RATE){
//逐帧遍历,只获取需要的
doGrabFramesByTraverseFrames(fFmpegFrameGrabber,frameRate,storePath,sourceFiles,videoRate);
}else {
//稍后会说明
doGrabFramesBySetTimestamp(fFmpegFrameGrabber,frameRate,storePath,sourceFiles,videoRate);
}
}
/**
* 产生的所有文件都暂时以本地存储为主
* fFmpegFrameGrabber ffmpeg抽帧工具
* videoRate 视频帧率
* storePath 为统一放置抽帧图片使用
* sourceFiles 抽帧之后图片存放本地位置
*
*/
void doGrabFramesByTraverseFrames(FFmpegFrameGrabber fFmpegFrameGrabber, double frameRate, String storePath, List<String> sourceFiles, double videoRate) throws FFmpegFrameGrabber.Exception {
Java2DFrameConverter converter = new Java2DFrameConverter();
int lengthInVideoFrames = fFmpegFrameGrabber.getLengthInFrames();
int frameGapCount = (int) Math.ceil(lengthInVideoFrames * frameRate / videoRate);
int frameGap = lengthInVideoFrames / frameGapCount;
Frame f;
String path;
boolean flag;
for (int i = 1; i <= frameGapCount; i++){
flag = false;
// 每frameGap取1帧
for (int j = 1; j <= frameGap; j++) {
f = fFmpegFrameGrabber.grabImage();
path = String.format("%s/%s", storePath, i + ".jpg");
doExecuteFrame(f,path,converter);
if (PicDetectionUtil.checkPicAvailableBySourcePath(path) && !flag){//图片检测,看你实际使用,并不必须
sourceFiles.add(path);
flag = true;
}
}
}
//考虑总帧数不能整除情况
if (lengthInVideoFrames > frameGap * frameGapCount){
int diff = lengthInVideoFrames - frameGap * frameGapCount;
while (diff > 0){
f = fFmpegFrameGrabber.grabImage();
path = String.format("%s/%s", storePath, frameGapCount + 1 + ".jpg");
doExecuteFrame(f,path,converter);
if (PicDetectionUtil.checkPicAvailableBySourcePath(path)){
sourceFiles.add(path);
break;
}
diff--;
}
}
converter.close();
}
在基于这段代码以及实际情况,输入帧率一般都是1,那么在整个视频时长足够长的情况下,逐帧读取是不是一个效率相对较慢的方法呢
1、获取视频的总帧数,循环遍历,根据入参帧率和视频帧率的数量关系,对满足一定条件的帧进行保存,也就是我一开始实现的方案
2、获取视频的总时长,根据根据入参帧率和视频帧率的数量关系计算出时间间隔,根据时间间隔通过setTimeStamp获取所要保存的帧数
第二种方案实现:
private static void doGrabFramesBySetTimestamp(FFmpegFrameGrabber fFmpegFrameGrabber, double frameRate, String storePath, List<String> sourceFiles, double videoRate) throws FFmpegFrameGrabber.Exception {
Java2DFrameConverter converter = new Java2DFrameConverter();
int lengthInVideoFrames = fFmpegFrameGrabber.getLengthInFrames();
int frameGapCount = (int) Math.ceil(lengthInVideoFrames * frameRate / videoRate);
long lengthInTime = fFmpegFrameGrabber.getLengthInTime();
Frame f;
String path;
double v = lengthInTime / ( 1.0 * frameGapCount) ;
int idx = 1;
for (long i = 1L; i <= lengthInTime; i += Math.ceil(v)){
fFmpegFrameGrabber.setTimestamp(i);
f = fFmpegFrameGrabber.grabImage();
path = String.format("%s/%s", storePath, idx + ".jpg");
idx++;
doExecuteFrame(f,path,converter);
if (PicDetectionUtil.checkPicAvailableBySourcePath(path)){
sourceFiles.add(path);
}
}
if (idx == frameGapCount){
fFmpegFrameGrabber.setTimestamp(lengthInTime);
f = fFmpegFrameGrabber.grabImage();
path = String.format("%s/%s", storePath, idx + ".jpg");
doExecuteFrame(f,path,converter);
if (PicDetectionUtil.checkPicAvailableBySourcePath(path)){
//图片检测,不是必须
sourceFiles.add(path);
}
}
converter.close();
}
两种方案的效率,实际与视频的关键帧数相关,如果所需抽帧的视频的关键帧所摆放的位置恰巧每次定位到指定时间戳之后都需要往前溯关键帧的位置,反而效率会不如原先的方案好。
笔者做了一个简单实验,在帧率从1到视频帧率的区间变化,两种方案的耗时对比,实验结果表示在小于等于视频帧率的1/2时,第二种方案更有效率,而帧率超过1/2后,第一种方案更有效率
这个实验还不具有特别大的说服性,笔者最后是通过设置一个阈值来决定抽帧方案
double videoRate = fFmpegFrameGrabber.getFrameRate();
if (frameRate >= videoRate){
//每一帧都获取
doGrabPerFrame(fFmpegFrameGrabber,storePath,sourceFiles);
}else if (frameRate >= ApolloConfig.VIDEO_RATE){
//逐帧遍历,只获取需要的
doGrabFramesByTraverseFrames(fFmpegFrameGrabber,frameRate,storePath,sourceFiles,videoRate);
}else {
//定位时间戳
doGrabFramesBySetTimestamp(fFmpegFrameGrabber,frameRate,storePath,sourceFiles,videoRate);
}
注:如果若刷到这篇博客的其他大佬有对这方面了解,欢迎指点迷津