likes
comments
collection
share

java cv流媒体处理高频抓帧

作者站长头像
站长
· 阅读数 18

什么是javacv

JavaCV 是一个基于 OpenCVFFmpegJava 接口库,它提供了许多图像和视频处理的功能。在本文中,我们将介绍如何使用 JavaCV 实现流媒体处理高频抓帧。

获取视频流

使用FFmpegFrameGrabber类获取视频流

FFmpegFrameGrabberJavaCV 中用于抓取视频帧的类,它是基于 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):设置视频帧的格式

除了 FFmpegFrameGrabberJavaCV 还提供了其他抓取视频帧的类,例如 OpenCVFrameGrabberJava2DFrameGrabber。根据不同的需求,我们可以选择使用不同的类来进行视频帧的抓取和处理。

设置视频流的URL、格式、帧率等参数

FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(input);
// tcp 形式 方式丢包过多
grabber.setOption("rtsp_transport", "tcp");
grabber.setFrameRate(24);
...

抓取视频帧

使用 JavaCV 抓取视频帧的基本步骤如下:

  1. 创建 FFmpegFrameGrabber 对象

    使用 FFmpegFrameGrabber 创建一个视频帧抓取器对象,并指定要抓取的视频文件路径:

    FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("input.mp4");
    
  2. 开始视频帧的抓取

    调用 grabber 对象的 start() 方法开始视频帧的抓取:

    grabber.start();
    
  3. 循环获取视频帧

    在一个循环中,使用 grabber 对象的 grab() 方法获取视频帧,直到抓取到视频帧为止:

    Frame frame;
    while ((frame = grabber.grab()) != null) {
        // 处理视频帧
    }
    
  4. 处理视频帧

    在循环中,使用获取到的视频帧进行处理,例如将视频帧保存到本地文件:

    OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
    Mat mat = converter.convert(frame);
    Imgcodecs.imwrite("output.jpg", mat);
    
  5. 停止视频帧的抓取

    在循环结束后,调用 grabber 对象的 stop() 方法停止视频帧的抓取:

    grabber.stop();
    

抓取视频帧存在问题

  1. 图片越来越模糊

错误原因:丢包严重导致,默认情况下,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 协议进行数据传输。


  1. 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博客 这篇文章说的比较靠谱,原因是处理速度跟不上输出速度,导致处理的图片是好几、几十秒之前的画面,缓存区爆满,最后导致程序报错。

解决此问题就要加快读取到图片的处理速度。

  1. 当同时读取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

ScheduledExecutorServiceJava 并发包中提供的一个接口,它继承了 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 执行定时任务或周期性任务的步骤一般如下:

  1. 创建一个 ScheduledExecutorService 对象。
  2. 创建一个实现 Runnable 接口的任务。
  3. 调用 ScheduledExecutorService 对象的 schedule()scheduleAtFixedRate() 或 scheduleWithFixedDelay() 方法来执行任务。
  4. 关闭 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。

这个类的使用方法一般如下:

  1. 创建一个 FrameGrabber 对象,用于获取视频流。
  2. 创建一个 VideoStreamProcessor 对象,将 FrameGrabber 对象传递给它。
  3. 调用 VideoStreamProcessor 对象的 start() 方法,开始获取视频帧。
  4. 循环调用 VideoStreamProcessor 对象的 getNextFrame() 方法,获取视频帧并进行处理。
  5. 调用 VideoStreamProcessor 对象的 stop() 方法,停止获取视频帧。

这样改进的优势是把视频帧的读取和图像的处理分离,同时在读取视频流的时候通过每隔n毫秒读取一次的方式来读取视频帧,即兼顾了视频帧读取的效率也避免了 while 循环的性能消耗问题。

图像处理

  • 例如进行人脸识别、运动检测等操作

结论

JavaCV 是一个基于 Java 的计算机视觉和机器学习库,它提供了一组用于处理图像和视频的工具和算法。在 JavaCV 中,可以使用 FFmpegFrameGrabber 类来获取视频流并抓取视频帧。但是,抓取视频帧存在一些问题,比如低效率和内存泄漏等。为了解决这些问题,我们可以使用高频抓帧优化技术,比如使用协程或者 ScheduledExecutorService。这些技术可以提高抓帧的效率,减少内存占用,并提高程序的稳定性。此外,JavaCV 还提供了一些图像处理的工具和算法,可以用于处理视频帧。通过使用 JavaCV,我们可以轻松地实现图像和视频处理的功能,并将其集成到我们的 Java 程序中。

转载自:https://juejin.cn/post/7226993037905068088
评论
请登录