笔者之前遇到一个抓取视频图片,然后视频图片倒置的问题,之后查了些博文找到解决办法。不过这不是主要的,主要的是javacv的代码笔者当时没有读懂,现在发一篇有详细注释的文档,分享一下使用心得。
一、先上源码,每一行都有注释
package com.muyichen.demo.javacv;
import org.bytedeco.javacv.*;
import org.bytedeco.opencv.opencv_core.IplImage;
import org.springframework.util.StringUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.bytedeco.opencv.global.opencv_core.cvFlip;
import static org.bytedeco.opencv.global.opencv_core.cvTranspose;
/**
* 抓取视频图片工具类
* @author muyichen
* @version 1.0
*/
public class FrameGrabberUtil {
/**
* 抓取视频图片工具
* @param filePath 视频文件路径(加上文件名)
* @param targetFilePath 取出图片的存放路径
* @param targetFileName 取出图片的名称(全名:当前参数+帧数)
* @param randomSize 取出多少张图片(数量)
* @throws FrameGrabber.Exception
*/
public static void randomGrabberFFmpegImage(String filePath, String targetFilePath, String targetFileName, int randomSize) throws FrameGrabber.Exception {
//根据路径读取视频文件
FFmpegFrameGrabber ff = FFmpegFrameGrabber.createDefault(filePath);
//启动FFmpegFrameGrabber(抓帧器)
ff.start();
//获取rotate属性
String rotate =ff.getVideoMetadata("rotate");
//获取视频中包含有多少帧
int ffLength = ff.getLengthInFrames();
//随机选出五个取帧位置
List<Integer> randomGrab = random(ffLength, randomSize);
//标记最后读取的位置(在这个位置跳出循环)
int maxRandomGrab =randomGrab.get(randomGrab.size() - 1);
//遍历随机选出来的帧位置
Frame f;
int i = 0;
while (i <ffLength) {
//取出对应位置的帧
f = ff.grabImage();
//当前帧是想要获取的帧则将其读取出来
if (randomGrab.contains(i)) {
//是否存在rotate属性,存在则需要对图片进行修正处理
if (!StringUtils.isEmpty(rotate)) {
//创建一个CVF帧-图片转换器
OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
//使用转换器将获取出的帧(Frame)对象转换为图片对象
IplImage src = converter.convert(f);
//将图片对象修正处理完成后,再转换为帧(Frame)对象
f = converter.convert(rotate(src, Integer.parseInt(rotate)));
}
//将帧对象输出为具体的图片文件
doExecuteFrame(f, targetFilePath, targetFileName, i);
}
//如果到需要的帧取完了则跳出循环
if (i >= maxRandomGrab) {
break;
}
i++;
};
ff.stop();
}
/**
* 将图片根据rotate属性还原为它原来的状态
* @param src 原始图片对象
* @param rotate 旋转角度
* @return
*/
public static IplImage rotate(IplImage src, int rotate) {
//创建一个和原图片宽高互换的图片框架
IplImage img = IplImage.create(src.height(), src.width(), src.depth(), src.nChannels());
//将原图片中元素的位置进行矩阵转置后放入新创建的图片框架中
cvTranspose(src, img);
//将转置好后的图片根据rotate属性的值旋转对应的度数(根据纵轴旋转rotate度数)
cvFlip(img, null, rotate);
//返回最终生成的图片
return img;
}
/**
* 将帧对象输出为具体的图片文件
* @param f 帧对象
* @param targetFilePath 输出图片文件存放路径
* @param targetFileName 输出图片文件名
* @param index 帧位置(视频的第几帧)
*/
public static void doExecuteFrame(Frame f, String targetFilePath, String targetFileName, int index) {
if (null == f || null == f.image) {
return;
}
//创建一个2D帧转换器
Java2DFrameConverter converter = new Java2DFrameConverter();
//设定图片的输出格式
String imageMat = "png";
//设定图片的全路径+图片名
String FileName = targetFilePath + File.separator + targetFileName + "_" + index + "." + imageMat;
//使用2D帧转换器将视频中的帧转换为图片缓冲对象
BufferedImage bi = converter.getBufferedImage(f);
//根据图片的全路径名创建一个图片文件
File output = new File(FileName);
try {
//使用图片IO将图片缓冲对象写入新创建的图片文件中
ImageIO.write(bi, imageMat, output);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 随机选出指定长度范围类的几个数字
* @param baseNum 随机选出的数字个数
* @param length 指定长度范围
* @return
*/
public static List<Integer> random(int baseNum, int length) {
//创建指定长度的接收数字集合对象
List<Integer> list = new ArrayList<>(length);
//当集合对象未满时,循环选出
while (list.size() < length) {
//获取指定范围类的随机数
Integer next = (int) (Math.random() * baseNum);
//判断集合中是否存在当前随机出来的数,有的话则跳出当前循环进入下一循环
if (list.contains(next)) {
continue;
}
//将指定范围随机数放入集合中
list.add(next);
}
//对集合进行排序
Collections.sort(list);
return list;
}
public static void main(String[] args) throws Exception {
randomGrabberFFmpegImage("/Users/muyichen/Downloads/0420上传.mp4", "/Users/muyichen/Downloads", "screenshot", 5);
}
}
二、使用注意事项
1、使用javacv之前记得倒入依赖
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.3</version>
</dependency>
注意各个版本之间的类方法位置可能不同,比如1.3.1版本中的IplImage是一个内部类,但是1.5.3的这个类却独立出来了。
2、注意视屏中的Rotate属性
Roate属性是导致图片由竖屏变横屏的主要原因,上述代码中有着详细的处理方法。
3、最容易忽视的一点:取帧(Frame)
使用FFmpegFrameGrabber的时候,需要将它当成一个流来处理,FFmpegFrameGrabber无法指定从确定位置取帧,只能一帧一帧的读取。grabImage()这个方法相当于流中的read()方法,它每读完一帧后都会从当前读取帧的下一帧位置开始读取。(也就是说如果要读取100帧位置的图片,那么他会先将前面的99帧都先读一遍后才能读到第100帧的图片)