视频抽帧实现

文章目录

    • 视频抽帧
      • 一、基本概念理解
        • 1、视频帧
        • 2、名词解释
      • 二、FFmpeg视频抽帧实践

视频抽帧

一、基本概念理解

一个视频由视频帧构成,每一帧在肉眼可见是一张图片成像

1、视频帧

帧的类型:

帧的类型主要参考 视频抽帧处理

  • I帧,Intra Picture,内编码帧,也就是关键帧。拥有完整的图像信息。I帧不需要依赖前后帧信息,可独立进行解码。
    P帧,predictive-frame,前向预测编码帧。P帧需要依赖前面的I帧或者P帧才能进行编解码,因为他存储的是当前帧画面与前一帧的差别,专业的说法是压缩了时间冗余信息,或者说提取了运动特性
  • B帧,bi-directional interpolatedprediction frame,双向预测内插编码帧。B帧存储的是本帧与前后帧的差别,因此带有B帧的视频在解码时的逻辑会更复杂些,CPU开销会更大
  • IDR帧,Instantaneous Decoding Refresh,即时解码刷新。IDR帧是一种特殊的I帧,它是为了服务于编解码而提出的概念,IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。

2、名词解释

  • 视频封装格式:相当于视频的后缀格式,mp4 mov mpeg,封装格式与视频编码格式无关,视频封装格式是视频播放软件为了能够播放视频文件而赋予视频文件的一种识别符号
  • 视频编码:对视频进行压缩的程序或设备,常见的编码方式有H.26x系列,MPEG系列

电脑中保存的视频文件实际经过了编码压缩,实际上修改视频文件的后缀,实际对视频本身并没有影响,后缀只是封装格式,视频本身压缩的信息是由编码格式决定。
视频原始流 -> 视频编码方式 -> 视频封装格式

  • 帧率:每秒所含帧数,现在视频帧率一般在22-25之间,也就是每秒所含帧数在22-25之间
  • 码率:视频文件在单位时间内使用的数据流量,它决定了视频的质量和大小,在实际应用中,还需要硬件的处理能力、带宽条件进行选择
  • 分辨率:也就是我们常说的1080p,决定图像显示的大小
  • 三者关系
    分辨率一定时,如果帧率越高,那么每秒的帧数越高,要承载的数据量越高,即码率越高;码率一定时,如果帧率越高,为了保证码率的承载数据量,每帧画面的数据量也就需要压缩,导致画面清晰度降低;帧率也就是每秒包含的帧数,当中包含三种帧 IPB (关键帧、前向预测帧、双向预测内插编码帧),其中I帧承载的数据量最大;关键帧的设置是可由客户端(如pr软件)设置的,导出视频时进行编码压缩,写入文件进行保存

二、FFmpeg视频抽帧实践

也就是说实际抽帧过程,并不是每一帧都是独立的个体,这还取决于关键帧的位置。
笔者这里使用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);
        }

注:如果若刷到这篇博客的其他大佬有对这方面了解,欢迎指点迷津

你可能感兴趣的:(音视频处理,音视频,ffmpeg,java)