java cv流媒体处理高频抓帧
什么是javacv
JavaCV
是一个基于 OpenCV
和 FFmpeg
的 Java
接口库,它提供了许多图像和视频处理的功能。在本文中,我们将介绍如何使用 JavaCV
实现流媒体处理高频抓帧。
获取视频流
使用FFmpegFrameGrabber类获取视频流
FFmpegFrameGrabber
是 JavaCV
中用于抓取视频帧的类,它是基于 FFmpeg
实现的。FFmpeg
是一个开源的视频和音频处理库,能够处理多种格式的音视频文件。
FFmpegFrameGrabber
提供了一系列方法来设置视频源、读取视频帧、获取视频信息等操作。通过FFmpegFrameGrabber
类,我们可以获取视频流、抓取视频帧并进行处理。
以下是 FFmpegFrameGrabber
的一些常用方法:
start()
:启动视频流读取器。stop()
:停止视频流读取器。grab()
:抓取一帧视频帧。getLengthInFrames()
:获取视频流中的总帧数。getFrameRate()
:获取视频流的帧率。setFrameRate()
:设置视频流的帧率。getPixelFormat()
:获取视频流的像素格式。getAudioChannels()
:获取音频流的通道数。getAudioSampleRate()
:获取音频流的采样率。getTimestamp()
:获取视频帧的时间戳setImageWidth(640)
:设置视频帧的大小setImageHeight(480)
:设置视频帧的大小setAudioChannels(2)
:设置视频帧的通道数setVideoCodec(avcodec.AV_CODEC_ID_H264)
:设置视频帧的编解码器setSampleRate(44100)
:设置视频帧的采样率setPixelFormat(avutil.AV_PIX_FMT_BGR24)
:设置视频帧的格式
除了 FFmpegFrameGrabber
,JavaCV
还提供了其他抓取视频帧的类,例如 OpenCVFrameGrabber
和Java2DFrameGrabber
。根据不同的需求,我们可以选择使用不同的类来进行视频帧的抓取和处理。
设置视频流的URL、格式、帧率等参数
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(input);
// tcp 形式 方式丢包过多
grabber.setOption("rtsp_transport", "tcp");
grabber.setFrameRate(24);
...
抓取视频帧
使用 JavaCV
抓取视频帧的基本步骤如下:
-
创建
FFmpegFrameGrabber
对象使用
FFmpegFrameGrabber
创建一个视频帧抓取器对象,并指定要抓取的视频文件路径:FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("input.mp4");
-
开始视频帧的抓取
调用
grabber
对象的start()
方法开始视频帧的抓取:grabber.start();
-
循环获取视频帧
在一个循环中,使用
grabber
对象的grab()
方法获取视频帧,直到抓取到视频帧为止:Frame frame; while ((frame = grabber.grab()) != null) { // 处理视频帧 }
-
处理视频帧
在循环中,使用获取到的视频帧进行处理,例如将视频帧保存到本地文件:
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat(); Mat mat = converter.convert(frame); Imgcodecs.imwrite("output.jpg", mat);
-
停止视频帧的抓取
在循环结束后,调用
grabber
对象的stop()
方法停止视频帧的抓取:grabber.stop();
抓取视频帧存在问题
- 图片越来越模糊
错误原因:丢包严重导致,默认情况下,JavaCV使用RTSP传输协议中的UDP方式进行数据传输。
在使用 JavaCV
抓取 RTSP
视频流时,有些视频服务器只支持使用 TCP
协议进行数据传输,如果使用 UDP
协议可能会导致视频流中断或者无法正常播放。
默认情况下,JavaCV
使用 RTSP
传输协议中的 UDP
方式进行数据传输,如果视频服务器不支持 UDP
协议,则需要手动设置使用 TCP
协议进行数据传输。设置方法如下:
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("rtsp://example.com/stream");
grabber.setOption("rtsp_transport", "tcp");
使用 TCP
协议进行数据传输会增加一些网络传输开销,但可以保证数据传输的可靠性。因此,如果您遇到 RTSP
视频流无法正常播放或者中断的问题,可以尝试设置使用 TCP
协议进行数据传输。
- ffmpeg报错
error while decoding MB 52 46, bytestream -7
错误原因:视频编码中的错误,通常是由于视频文件损坏或编码错误导致的。这个错误信息指出在解码视频时发生了一个错误,MB 52 46 是指解码器在处理视频帧时遇到了错误的宏块,bytestream -7 表示解码器在处理视频时遇到了错误的字节流。
此错误查阅了很多资料 Python使用小教程01——[h264 @ 0x55abeda05080] error while decoding MB 0 14, bytestream 104435_puffdoudou的博客-CSDN博客 这篇文章说的比较靠谱,原因是处理速度跟不上输出速度,导致处理的图片是好几、几十秒之前的画面,缓存区爆满,最后导致程序报错。
解决此问题就要加快读取到图片的处理速度。
- 当同时读取10路左右流时机器卡死
问题原因:在抓取视频桢一节中我们使用while循环处理抓帧,可能会存在以下问题:
-
CPU
和内存资源不足同时处理多路视频流会占用大量的
CPU
和内存资源,如果计算机的CPU
和内存资源不足,就可能导致计算机卡死。因此,在处理多路视频流时,需要确保计算机具有足够的CPU
和内存资源。 -
磁盘
I/O
速度不足如果同时处理多路视频流,可能会对磁盘
I/O
速度造成很大压力,导致磁盘I/O
速度不足,从而导致计算机卡死。因此,在处理多路视频流时,需要确保计算机的磁盘I/O
速度足够快。 -
程序逻辑问题
如果程序逻辑存在问题,例如没有及时释放内存、处理速度跟不上视频帧的获取速度等,也可能导致计算机卡死。因此,在处理多路视频流时,需要注意程序逻辑的正确性和效率。
- 使用
FrameGrabber
类的grab()
方法抓取视频帧 - 将视频帧转换为
Mat
对象进行处理
- 使用
高频抓帧优化
使用协程
下面是一个使用协程处理的java例子:
/**
* @author: fankk
* @create: 2023-04-21 16:22:11
* @description:
*/
public class Test4 {
public static void record() throws FrameRecorder.Exception, org.bytedeco.javacv.FrameGrabber.Exception{
String input = "rtsp://172.168.250.4/lg_gw/test";
List<Fiber> fiberList = Lists.newArrayList();
for(int i = 0; i < 1; i++) {
int finalI = i;
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(input + (i + 1));
grabber.setOption("rtsp_transport", "tcp");
grabber.setPixelFormat(1);
grabber.setFrameRate(3);
// grabber.setOption("video_colorspace", "bt709");
// grabber.setOption("pixel_format", "yuv420p");
// grabber.setPixelFormat(AV_PIX_FMT_RGB24);
try {
grabber.start();
} catch (FFmpegFrameGrabber.Exception e) {
e.printStackTrace();
}
Integer width=grabber.getImageWidth();
Integer height=grabber.getImageHeight();
FFmpegFrameRecorder recorder =new FFmpegFrameRecorder(output,width,height,0);
int ftp = (int)grabber.getFrameRate();
recorder.setFormat("image2");
recorder.setOption("update", "0");
try {
recorder.start();
} catch (FFmpegFrameRecorder.Exception e) {
e.printStackTrace();
}
CanvasFrame canvas = new CanvasFrame("图像预览" + finalI);// 新建一个窗口
canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
int finalI1 = i;
Fiber<String> fiber = new Fiber<>(() -> {
Frame frame = null;
int delay = 1000 / ftp;
try {
while ((frame = grabber.grabImage()) != null) {
canvas.showImage(frame);
Frame finalFrame = frame;
new Fiber(() -> {
saveImg(finalFrame, finalI1);
}).start();
Fiber.sleep(delay);
}
}catch (Exception e) {
e.printStackTrace();
}
});
fiberList.add(fiber);
}
for (int i = 0; i < fiberList.size(); i++) {
fiberList.get(i).start();
}
try {
Thread.sleep(3000000000000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void saveImg(Frame frame, Integer finalI1) {
try {
RenderedImage renderedImage = frameToBufferedImage(frame);
@Cleanup ByteArrayOutputStream fs = new ByteArrayOutputStream();
ImageIO.write(renderedImage, "jpg", fs);
String base64Img = Base64Encoder.encode(fs.toByteArray()).replaceAll("[\s*\t\n\r]", "");
downloadImg(base64Img, "F:/img1/img" + finalI1);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 帧转为流
*
* @param frame
* @return
*/
private static RenderedImage frameToBufferedImage(Frame frame) {
//创建BufferedImage对象
Java2DFrameConverter converter = new Java2DFrameConverter();
BufferedImage bufferedImage = converter.getBufferedImage(frame);
return bufferedImage;
}
/**
* base64转图片
*
* @param base64str base64码
* @param savePath 图片路径
* @return boolean 判断是否成功下载到本地
*/
private static boolean downloadImg(String base64str, String savePath) throws IOException {
//对字节数组字符串进行Base64解码并生成图片
if (base64str == null) {
return false;
}
//Base64解码
byte[] b = Base64Decoder.decode(base64str);
for (int i = 0; i < b.length; ++i) {
//调整异常数据
if (b[i] < 0) {
b[i] += 256;
}
}
// 判断路径是否不存在,不存在就创建文件夹
File fileDir = new File(savePath);
if (!fileDir.exists() && !fileDir.isDirectory()) {
fileDir.mkdirs();
}
// 生成一个空文件,自定义图片的名字
File file = new File(savePath +"/" + System.currentTimeMillis() + ".jpg");
if (!file.exists()) {
file.createNewFile();
}
//生成jpg图片
OutputStream out = new FileOutputStream(file.getPath());
out.write(b);
out.flush();
out.close();
return true;
}
public static void main(String[] args)
try {
record();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上面的例子中,使用 fiber
在每次抓取图像后休眠n
,在测试过程中发现能很好的解决同时抓取 20
路摄像头时的资源消耗问题。但引入了协程又增加了系统的复杂性。本文发布于 juejin 谢绝转载。
使用 ScheduledExceutorService
ScheduledExecutorService
是Java
并发包中提供的一个接口,它继承了ExecutorService
接口,提供了一些方法可以用来执行定时任务或周期性任务。与
Timer
类相比,ScheduledExecutorService
更加灵活,更加可靠,因为它使用了线程池来执行任务,可以避免任务间的相互影响,还可以动态调整线程池的大小,以适应任务的负载情况;而Timer
类则是单线程执行任务,任务之间可能会相互干扰,而且无法动态调整线程池的大小。
ScheduledExecutorService 简介
ScheduledExecutorService
接口中常用的方法有:
schedule(Runnable command, long delay, TimeUnit unit)
:在指定的时间后执行一次任务。scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:在指定的时间后开始执行任务,并以指定的周期执行任务。scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
:在指定的时间后开始执行任务,并以指定的延迟时间执行任务。
使用 ScheduledExecutorService
执行定时任务或周期性任务的步骤一般如下:
- 创建一个
ScheduledExecutorService
对象。 - 创建一个实现
Runnable
接口的任务。 - 调用
ScheduledExecutorService
对象的schedule()
、scheduleAtFixedRate()
或scheduleWithFixedDelay()
方法来执行任务。 - 关闭
ScheduledExecutorService
对象。
例如,以下代码演示了如何使用 ScheduledExecutorService
执行一个定时任务:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
};
executor.schedule(task, 5, TimeUnit.SECONDS);
executor.shutdown();
这段代码会创建一个 ScheduledExecutorService
对象,然后创建一个实现 Runnable
接口的任务,最后调用 schedule()
方法,在 5 秒后执行任务。执行完任务后,调用 shutdown()
方法关闭 ScheduledExecutorService
对象。
使用 ScheduledExecutorService
改进抓帧
/**
* @author: fankk
* @create: 2023-04-21 16:22:11
* @description:
*/
public class VideoStreamProcessor {
private static final int QUEUE_CAPACITY = 6;
private CircularFifoQueue<BufferedImage> frameQueue;
private Java2DFrameConverter converter;
private FrameGrabber grabber;
private ScheduledExecutorService executor;
private boolean isRunning;
public VideoStreamProcessor(FrameGrabber grabber) {
this.grabber = grabber;
this.converter = new Java2DFrameConverter();
this.frameQueue = new CircularFifoQueue<>(QUEUE_CAPACITY);
this.executor = new ScheduledThreadPoolExecutor(1);
this.isRunning = true;
}
public void start() {
executor.scheduleAtFixedRate(() -> {
try {
BufferedImage image = converter.convert(grabber.grab());
if (image != null) {
if (!frameQueue.offer(image)) {
System.err.println("Frame queue is full!");
} else {
System.out.println("入队成功!" + System.currentTimeMillis());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 10, TimeUnit.MILLISECONDS);
}
public void stop() {
this.isRunning = false;
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
grabber.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
public BufferedImage getNextFrame() throws InterruptedException {
return frameQueue.poll();
}
}
这个类是一个视频流处理器,它使用了 JavaCV
库来获取视频流并将视频帧转换成 BufferedImage
对象,然后将这些帧存储在一个循环队列中,供其他模块使用。本文发布于 juejin 谢绝转载。
这个类的主要属性和方法如下:
frameQueue
:循环队列,用于存储视频帧。converter
:JavaCV 库中的一个工具类,用于将视频帧转换成 BufferedImage 对象。grabber
:JavaCV 库中的一个类,用于获取视频流。executor
:ScheduledExecutorService 对象,用于定时抓取视频帧。isRunning
:标志位,用于表示视频流是否正在运行。start()
:启动视频流处理器,开始定时抓取视频帧。stop()
:停止视频流处理器,停止抓取视频帧。getNextFrame()
:从视频帧队列中获取下一个视频帧。
在这个类的构造函数中,它初始化了循环队列、Java2DFrameConverter 对象、FrameGrabber 对象、ScheduledExecutorService 对象以及标志位。
在 start()
方法中,它通过调用 executor.scheduleAtFixedRate()
方法来定时抓取视频帧,抓取的时间间隔为 10 毫秒。在抓取视频帧后,它会将视频帧转换成 BufferedImage 对象,并将其存储在循环队列中。
在 stop()
方法中,它会停止视频流处理器的运行,并停止定时抓取视频帧。它首先关闭 ScheduledExecutorService 对象,等待 1 秒钟,然后停止 FrameGrabber 对象。
在 getNextFrame()
方法中,它从视频帧队列中获取下一个视频帧,并返回它。如果队列为空,则返回 null。
这个类的使用方法一般如下:
- 创建一个
FrameGrabber
对象,用于获取视频流。 - 创建一个
VideoStreamProcessor
对象,将 FrameGrabber 对象传递给它。 - 调用
VideoStreamProcessor
对象的start()
方法,开始获取视频帧。 - 循环调用
VideoStreamProcessor
对象的getNextFrame()
方法,获取视频帧并进行处理。 - 调用
VideoStreamProcessor
对象的stop()
方法,停止获取视频帧。
这样改进的优势是把视频帧的读取和图像的处理分离,同时在读取视频流的时候通过每隔n毫秒读取一次的方式来读取视频帧,即兼顾了视频帧读取的效率也避免了 while
循环的性能消耗问题。
图像处理
- 例如进行人脸识别、运动检测等操作
略
结论
JavaCV
是一个基于 Java
的计算机视觉和机器学习库,它提供了一组用于处理图像和视频的工具和算法。在 JavaCV
中,可以使用 FFmpegFrameGrabber
类来获取视频流并抓取视频帧。但是,抓取视频帧存在一些问题,比如低效率和内存泄漏等。为了解决这些问题,我们可以使用高频抓帧优化技术,比如使用协程或者 ScheduledExecutorService
。这些技术可以提高抓帧的效率,减少内存占用,并提高程序的稳定性。此外,JavaCV
还提供了一些图像处理的工具和算法,可以用于处理视频帧。通过使用 JavaCV
,我们可以轻松地实现图像和视频处理的功能,并将其集成到我们的 Java
程序中。
转载自:https://juejin.cn/post/7226993037905068088