原创

JavaCV开源计算机视觉库

温馨提示:
本文最后更新于 2024年11月30日,已超过 140 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

一.简介

JavaCV 是一个强大的 Java 库,它提供了对多个流行的计算机视觉和多媒体处理库(如 OpenCV、FFmpeg、libdc1394 等)的绑定,使得开发者能够在 Java 应用程序中方便地进行图像处理、视频处理、实时流媒体传输、摄像头访问以及深度学习等任务。通过 JavaCV,开发者可以利用这些底层 C/C++ 库的强大功能,同时享受 Java 语言的跨平台性和易用性。JavaCV 还包括了 javacpp 工具,简化了 Java 与 C++ 之间的交互,从而进一步增强了其功能和灵活性。

在视频处理中,选择合适的编码格式至关重要。以下是一些常用的视频流编码格式及其特点:

H.264 (Advanced Video Coding, AVC)

  • 压缩效率:提供较高的压缩比,在保持良好画质的同时可以显著减少文件大小。
  • 图像质量:在相同的比特率下,H.264通常能提供比MPEG-4 Part 2更好的图像质量。
  • 计算复杂度:相对较高,需要较强的处理器来解码。
  • 应用场景
    • 广泛应用于互联网流媒体、蓝光光盘、数字电视广播等。
    • 适用于各种设备,包括移动设备、桌面计算机、智能电视等。

H.265 (High Efficiency Video Coding, HEVC)

  • 压缩效率:比H.264更高的压缩效率,可以在相同的图像质量下减少约50%的数据量。
  • 图像质量:在相同的比特率下,H.265能够提供比H.264更高质量的图像。
  • 计算复杂度:由于采用了更复杂的算法,H.265的编解码复杂度更高,对硬件的要求也更高。
  • 应用场景
    • 适用于4K和8K超高清视频。
    • 用于下一代视频编码标准。
    • 适合于高分辨率视频和需要高效带宽利用的应用场景。

MJPEG (Motion JPEG)

  • 压缩效率:相对较低,因为每一帧都独立压缩,没有帧间预测。
  • 图像质量:取决于JPEG压缩的质量设置,可以有较好的图像质量,但通常不如H.264或H.265。
  • 计算复杂度:相对较低,易于实现和解码。
  • 应用场景
    • 常用于监控摄像头、网络摄像头等,因为它允许逐帧访问,便于实时处理和分析。
    • 也用于一些不需要高压缩比但要求低延迟的应用。

二.实现

在 pom.xml 文件中添加 JavaCV 的依赖项,这个依赖包含了 JavaCV 所需的所有核心库,包括OpenCV、FFmpeg、libdc1394 等。

<!-- JavaCV 核心库 -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.9</version>
</dependency>

2.1.屏幕共享

ScreenCaptureServer 负责从本地计算机上捕获屏幕内容,并将捕获到的内容编码为 JPEG 格式,然后通过网络发送给客户端。以下是 ScreenCaptureServer.java 的实现示例:

import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

public class ScreenCaptureServer {
    // 帧数据队列
    private static final LinkedBlockingQueue<byte[]> frameDataQueue = new LinkedBlockingQueue<>(100);
    // 线程池大小
    private static final int THREAD_POOL_SIZE = 4;
    // 压缩质量,范围0.0-1.0
    private static final float COMPRESSION_QUALITY = 0.7f;

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8088)) {
            System.out.println("等待客户端连接...");

            // 接受客户端连接
            try (Socket clientSocket = serverSocket.accept()) {
                System.out.println("客户端已连接");

                // 根据操作系统选择合适的 FFmpeg 参数
                String input;
                if (System.getProperty("os.name").toLowerCase().contains("windows")) {
                    input = "desktop";
                } else if (System.getProperty("os.name").toLowerCase().contains("mac")) {
                    // macOS 使用 1 代表屏幕
                    input = "1";
                } else {
                    // 假设是 Linux,获取屏幕分辨率
                    String resolution = "1920x1080";
                    input = "x11grab::0.0+0,0?video_size=" + resolution + "&framerate=30";
                }

                // 使用 FFmpeg 捕获屏幕
                try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(input)) {
                    if (System.getProperty("os.name").toLowerCase().contains("windows")) {
                        // Windows 上的桌面捕获
                        grabber.setFormat("gdigrab");
                    } else if (System.getProperty("os.name").toLowerCase().contains("mac")) {
                        // macOS 上的桌面捕获
                        grabber.setFormat("avfoundation");
                    } else {
                        // Linux 上的桌面捕获
                        grabber.setFormat("x11grab");
                    }
                    // 设置原始图像宽度
                    grabber.setImageWidth(2560);
                    // 设置原始图像高度
                    grabber.setImageHeight(1600);
                    // 设置帧率
                    grabber.setFrameRate(30);

                    // 设置 FFmpeg 日志级别
                    grabber.setOption("loglevel", "info");
                    // 打印详细报告
                    grabber.setOption("report", "1");

                    grabber.start();

                    // 获取输出流
                    final OutputStream outputStream = clientSocket.getOutputStream();

                    // 创建线程池
                    ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

                    // 开始捕获帧并转换为 BufferedImage
                    Thread captureThread = new Thread(() -> {
                        Java2DFrameConverter converter = new Java2DFrameConverter();
                        while (true) {
                            try {
                                Frame frame = grabber.grab();
                                if (frame == null || frame.image == null || frame.imageWidth <= 0 || frame.imageHeight <= 0) {
                                    System.err.println("未能获取有效帧。帧详细信息:图像=" + frame + ", 图像宽度=" + frame.imageWidth + ", 图像高度=" + frame.imageHeight);
                                    continue;
                                }

                                // 将帧转换为 BufferedImage
                                BufferedImage originalImage = converter.getBufferedImage(frame);

                                // 提交图像处理任务到线程池
                                executor.submit(() -> {
                                    try {
                                        // 缩放图像,目标宽度
                                        int targetWidth = 1920;
                                        // 目标高度
                                        int targetHeight = 1200;
                                        BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, originalImage.getType());
                                        Graphics2D g2d = scaledImage.createGraphics();
                                        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                                        g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
                                        g2d.dispose();

                                        // 将 BufferedImage 转换为字节数组,并设置压缩质量
                                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
                                        if (writers.hasNext()) {
                                            ImageWriter writer = writers.next();
                                            ImageWriteParam param = writer.getDefaultWriteParam();
                                            param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                                            param.setCompressionQuality(COMPRESSION_QUALITY);
                                            writer.setOutput(new MemoryCacheImageOutputStream(baos));
                                            writer.write(null, new javax.imageio.IIOImage(scaledImage, null, null), param);
                                            writer.dispose();
                                        }
                                        byte[] data = baos.toByteArray();

                                        // 将帧数据放入队列
                                        frameDataQueue.put(data);
                                    } catch (InterruptedException | IOException e) {
                                        e.printStackTrace();
                                    }
                                });
                            } catch (FFmpegFrameGrabber.Exception e) {
                                e.printStackTrace();
                                System.err.println("在捕获帧时发生错误: " + e.getMessage());
                                break;
                            }
                        }
                    });

                    // 开始发送帧数据
                    Thread sendThread = new Thread(() -> {
                        long startTime = System.currentTimeMillis();
                        long totalBytesSent = 0;
                        while (true) {
                            try {
                                // 从队列中获取帧数据
                                byte[] data = frameDataQueue.take();

                                // 发送帧大小
                                int size = data.length;
                                outputStream.write(ByteBuffer.allocate(4).putInt(size).array());

                                // 发送帧数据
                                outputStream.write(data);
                                outputStream.flush();

                                // 更新总发送数据量
                                totalBytesSent += size;

                                // 打印每次发送的帧大小
                                System.out.println("已发送帧大小: " + size + " 字节");

                                // 检查是否已经过了一秒
                                long currentTime = System.currentTimeMillis();
                                if (currentTime - startTime >= 1000) {
                                    // 打印每秒发送的数据量
                                    System.out.println("每秒发送数据量: " + totalBytesSent / 1024.0 + " KB");

                                    // 重置计数器
                                    totalBytesSent = 0;
                                    startTime = currentTime;
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                                break;
                            }
                        }
                    });

                    // 启动所有线程
                    captureThread.start();
                    sendThread.start();

                    // 等待所有线程结束
                    captureThread.join();
                    sendThread.join();

                    // 关闭线程池
                    executor.shutdown();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ScreenDisplayClient264 负责从本地计算机上捕获屏幕内容,并将捕获到的内容编码为 H.264 格式,然后通过网络发送给客户端。以下是 ScreenDisplayClient264.java 的实现示例:

import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 一个用于接收 H.264 流并通过 CanvasFrame 显示的客户端。
 */
public class ScreenDisplayClient264 {
    // 线程池大小
    private static final int THREAD_POOL_SIZE = 2;
    // 图像队列,用于存放从服务器接收到的图像
    private static final LinkedBlockingQueue<BufferedImage> imageQueue = new LinkedBlockingQueue<>(100);

    public static void main(String[] args) {
        try (Socket socket = new Socket("192.168.1.7", 8088);
             InputStream inputStream = socket.getInputStream()) {

            // 创建窗口
            CanvasFrame canvas = new CanvasFrame("Remote Screen");
            canvas.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);

            // 获取默认屏幕设备
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            int screenWidth = gd.getDisplayMode().getWidth();
            int screenHeight = gd.getDisplayMode().getHeight();

            // 设置CanvasFrame的大小为屏幕分辨率
            canvas.setSize(screenWidth, screenHeight);

            // 将CanvasFrame放置于屏幕中央(如果未全屏)
            canvas.setLocationRelativeTo(null);

            // 设置CanvasFrame为全屏模式
            if (gd.isFullScreenSupported()) {
                // 确保CanvasFrame是可见的
                canvas.setVisible(true);
                gd.setFullScreenWindow(canvas);
            } else {
                System.out.println("全屏模式不被支持");
            }

            // 创建线程池
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

            // 初始化 FFmpegFrameGrabber 和 Java2DFrameConverter
            Java2DFrameConverter converter = new Java2DFrameConverter();

            // 开始接收帧数据
            Thread receiverThread = createReceiverThread(inputStream, executor, converter);

            // 开始显示帧
            Thread displayThread = createDisplayThread(canvas);

            // 启动接收和显示线程
            receiverThread.start();
            displayThread.start();

            // 等待所有线程结束
            receiverThread.join();
            displayThread.join();

            // 关闭线程池
            executor.shutdown();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建一个接收帧数据的线程。
     *
     * @param inputStream 输入流
     * @param executor    线程池
     * @param converter   Java2DFrameConverter 实例
     * @return 接收帧数据的线程
     */
    private static Thread createReceiverThread(InputStream inputStream, ExecutorService executor, Java2DFrameConverter converter) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 读取帧大小(4字节整数)
                    byte[] sizeBytes = new byte[4];
                    int bytesRead = 0;
                    while (bytesRead < 4) {
                        int read = inputStream.read(sizeBytes, bytesRead, 4 - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 解析帧大小
                    int size = ByteBuffer.wrap(sizeBytes).asIntBuffer().get();

                    // 检查 size 是否为负数或异常大
                    if (size < 0 || size > 10 * 1024 * 1024) {
                        System.err.println("无效的帧大小: " + size);
                        continue;
                    }

                    System.out.println("接收到了帧大小: " + size + "字节");

                    // 读取帧数据
                    byte[] data = new byte[size];
                    bytesRead = 0;
                    while (bytesRead < size) {
                        int read = inputStream.read(data, bytesRead, size - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 提交解码任务到线程池
                    executor.submit(() -> {
                        try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
                            FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(bais);
                            // 设置 FFmpeg 日志级别为 error,以减少日志输出
                            grabber.setOption("loglevel", "error");
                            // 设置输入流格式为 H.264
                            grabber.setFormat("h264");
                            // 增加分析时间和探测大小
                            grabber.setOption("analyzeduration", "5000000");
                            grabber.setOption("probesize", "5000000");
                            // 启动抓取器
                            grabber.start();

                            Frame frame;
                            while ((frame = grabber.grab()) != null) {
                                // 将帧转换为 BufferedImage
                                BufferedImage image = converter.getBufferedImage(frame);
                                // 将解码后的图像放入队列
                                imageQueue.put(image);
                            }
                        } catch (IOException | InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                    // 如果发生错误,则退出循环
                    break;
                }
            }
        });
    }

    /**
     * 创建一个显示帧的线程。
     *
     * @param canvas CanvasFrame 实例
     * @return 显示帧的线程
     */
    private static Thread createDisplayThread(CanvasFrame canvas) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 从队列中获取图像
                    BufferedImage image = imageQueue.take();
                    // 显示帧
                    canvas.showImage(image);
                    // 控制每秒30帧的显示速率,1000毫秒 / 30 ≈ 33.3毫秒
                    canvas.waitKey(33);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

ScreenDisplayClient 负责从服务器接收编码后的屏幕内容数据,对接收到的JPEG数据进行解码,并在本地显示器上显示出来。

import org.bytedeco.javacv.CanvasFrame;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

public class ScreenDisplayClient {
    // 线程池大小
    private static final int THREAD_POOL_SIZE = 2;
    // 图像队列,用于存放从服务器接收到的图像
    private static final LinkedBlockingQueue<BufferedImage> imageQueue = new LinkedBlockingQueue<>(100);

    public static void main(String[] args) {
        try (Socket socket = new Socket("192.168.1.7", 8088);
             InputStream inputStream = socket.getInputStream()) {

            // 创建窗口
            CanvasFrame canvas = new CanvasFrame("Remote Screen");
            canvas.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);

            // 获取默认屏幕设备
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            int screenWidth = gd.getDisplayMode().getWidth();
            int screenHeight = gd.getDisplayMode().getHeight();

            // 设置CanvasFrame的大小为屏幕分辨率
            canvas.setSize(screenWidth, screenHeight);

            // 将CanvasFrame放置于屏幕中央(如果未全屏)
            canvas.setLocationRelativeTo(null);

            // 设置CanvasFrame为全屏模式
            if (gd.isFullScreenSupported()) {
                canvas.setVisible(true);  // 确保CanvasFrame是可见的
                gd.setFullScreenWindow(canvas);
            } else {
                System.out.println("全屏模式不被支持");
            }

            // 创建线程池
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

            // 开始接收帧数据
            Thread receiverThread = new Thread(() -> {
                while (true) {
                    try {
                        // 读取帧大小(4字节整数)
                        byte[] sizeBytes = new byte[4];
                        int bytesRead = 0;
                        while (bytesRead < 4) {
                            int read = inputStream.read(sizeBytes, bytesRead, 4 - bytesRead);
                            if (read == -1) {
                                throw new IOException("远程主机关闭了连接。");
                            }
                            bytesRead += read;
                        }

                        // 解析帧大小
                        int size = ByteBuffer.wrap(sizeBytes).asIntBuffer().get();

                        // 检查 size 是否为负数或异常大
                        if (size < 0 || size > 10 * 1024 * 1024) { // 假设最大帧大小为 10MB
                            System.err.println("无效的帧大小: " + size);
                            continue; // 跳过这次循环,继续下一次
                        }

                        System.out.println("接收到了帧大小: " + size);

                        // 读取帧数据
                        byte[] data = new byte[size];
                        bytesRead = 0;
                        while (bytesRead < size) {
                            int read = inputStream.read(data, bytesRead, size - bytesRead);
                            if (read == -1) {
                                throw new IOException("远程主机关闭了连接。");
                            }
                            bytesRead += read;
                        }

                        System.out.println("接收到了帧数据: " + data.length + " 字节");

                        // 提交解码任务到线程池
                        executor.submit(() -> {
                            try {
                                // 将字节数组转换为 BufferedImage
                                BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));
                                if (image == null) {
                                    System.err.println("无法从接收到的数据中解码图像。");
                                    return;
                                }

                                // 将解码后的图像放入队列
                                imageQueue.put(image);
                            } catch (IOException | InterruptedException e) {
                                e.printStackTrace();
                            }
                        });
                    } catch (IOException e) {
                        e.printStackTrace();
                        break; // 如果发生错误,则退出循环
                    }
                }
            });

            // 开始显示帧
            Thread displayThread = new Thread(() -> {
                while (true) {
                    try {
                        // 从队列中获取图像
                        BufferedImage image = imageQueue.take();
                        // 显示帧
                        canvas.showImage(image);
                        // 控制每秒30帧的显示速率
                        canvas.waitKey(33); // 1000毫秒 / 30 ≈ 33.3毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

            // 启动接收和显示线程
            receiverThread.start();
            displayThread.start();

            // 等待所有线程结束
            receiverThread.join();
            displayThread.join();

            // 关闭线程池
            executor.shutdown();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ScreenDisplayClient264 负责从服务器接收编码后的屏幕内容数据,对接收到的H.264数据进行解码,并在本地显示器上显示出来。

import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 一个用于接收 H.264 流并通过 CanvasFrame 显示的客户端。
 */
public class ScreenDisplayClient264 {
    // 线程池大小
    private static final int THREAD_POOL_SIZE = 2;
    // 图像队列,用于存放从服务器接收到的图像
    private static final LinkedBlockingQueue<BufferedImage> imageQueue = new LinkedBlockingQueue<>(100);

    public static void main(String[] args) {
        try (Socket socket = new Socket("192.168.1.7", 8088);
             InputStream inputStream = socket.getInputStream()) {

            // 创建窗口
            CanvasFrame canvas = new CanvasFrame("Remote Screen");
            canvas.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);

            // 获取默认屏幕设备
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            int screenWidth = gd.getDisplayMode().getWidth();
            int screenHeight = gd.getDisplayMode().getHeight();

            // 设置CanvasFrame的大小为屏幕分辨率
            canvas.setSize(screenWidth, screenHeight);

            // 将CanvasFrame放置于屏幕中央(如果未全屏)
            canvas.setLocationRelativeTo(null);

            // 设置CanvasFrame为全屏模式
            if (gd.isFullScreenSupported()) {
                // 确保CanvasFrame是可见的
                canvas.setVisible(true);
                gd.setFullScreenWindow(canvas);
            } else {
                System.out.println("全屏模式不被支持");
            }

            // 创建线程池
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

            // 初始化 FFmpegFrameGrabber 和 Java2DFrameConverter
            Java2DFrameConverter converter = new Java2DFrameConverter();

            // 开始接收帧数据
            Thread receiverThread = createReceiverThread(inputStream, executor, converter);

            // 开始显示帧
            Thread displayThread = createDisplayThread(canvas);

            // 启动接收和显示线程
            receiverThread.start();
            displayThread.start();

            // 等待所有线程结束
            receiverThread.join();
            displayThread.join();

            // 关闭线程池
            executor.shutdown();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建一个接收帧数据的线程。
     *
     * @param inputStream 输入流
     * @param executor    线程池
     * @param converter   Java2DFrameConverter 实例
     * @return 接收帧数据的线程
     */
    private static Thread createReceiverThread(InputStream inputStream, ExecutorService executor, Java2DFrameConverter converter) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 读取帧大小(4字节整数)
                    byte[] sizeBytes = new byte[4];
                    int bytesRead = 0;
                    while (bytesRead < 4) {
                        int read = inputStream.read(sizeBytes, bytesRead, 4 - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 解析帧大小
                    int size = ByteBuffer.wrap(sizeBytes).asIntBuffer().get();

                    // 检查 size 是否为负数或异常大
                    if (size < 0 || size > 10 * 1024 * 1024) {
                        System.err.println("无效的帧大小: " + size);
                        continue;
                    }

                    System.out.println("接收到了帧大小: " + size + "字节");

                    // 读取帧数据
                    byte[] data = new byte[size];
                    bytesRead = 0;
                    while (bytesRead < size) {
                        int read = inputStream.read(data, bytesRead, size - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 提交解码任务到线程池
                    executor.submit(() -> {
                        try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
                            FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(bais);
                            // 设置 FFmpeg 日志级别为 error,以减少日志输出
                            grabber.setOption("loglevel", "error");
                            // 设置输入流格式为 H.264
                            grabber.setFormat("h264");
                            // 增加分析时间和探测大小
                            grabber.setOption("analyzeduration", "5000000");
                            grabber.setOption("probesize", "5000000");
                            // 启动抓取器
                            grabber.start();

                            Frame frame;
                            while ((frame = grabber.grab()) != null) {
                                // 将帧转换为 BufferedImage
                                BufferedImage image = converter.getBufferedImage(frame);
                                // 将解码后的图像放入队列
                                imageQueue.put(image);
                            }
                        } catch (IOException | InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                    // 如果发生错误,则退出循环
                    break;
                }
            }
        });
    }

    /**
     * 创建一个显示帧的线程。
     *
     * @param canvas CanvasFrame 实例
     * @return 显示帧的线程
     */
    private static Thread createDisplayThread(CanvasFrame canvas) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 从队列中获取图像
                    BufferedImage image = imageQueue.take();
                    // 显示帧
                    canvas.showImage(image);
                    // 控制每秒30帧的显示速率,1000毫秒 / 30 ≈ 33.3毫秒
                    canvas.waitKey(33);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

2.2.实时视频流

实时视频捕获是一种从摄像头、屏幕或其他视频源获取视频流并在几乎无延迟的情况下处理和显示的技术。这种技术在许多应用中都非常关键,包括但不限于:视频会议、远程监控、直播等。以下是摄像头捕获视频流的示例。

CameraServer从本地摄像头捕获视频帧,并将捕获的视频帧编码为 H.264 格式。通过网络将编码后的视频数据发送给客户端。

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameGrabber;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 一个用于从摄像头捕获视频流并通过网络发送给客户端的服务器。
 */
public class CameraServer {
    // 帧数据队列,用于存放编码后的帧数据
    private static final LinkedBlockingQueue<byte[]> frameDataQueue = new LinkedBlockingQueue<>(100);

    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8088)) {
            System.out.println("等待客户端连接...");

            // 接受客户端连接
            try (Socket clientSocket = serverSocket.accept()) {
                System.out.println("客户端已连接");

                // 使用 OpenCV 捕获摄像头画面
                try (OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0)) {
                    // 设置原始图像宽度
                    int imageWidth = 1280; // 可以根据需要调整分辨率
                    // 设置原始图像高度
                    int imageHeight = 720; // 可以根据需要调整分辨率
                    // 设置帧率
                    int frameRate = 25; // 可以根据需要调整帧率

                    // 启动抓取器
                    grabber.start();

                    // 获取输出流
                    final OutputStream outputStream = clientSocket.getOutputStream();

                    // 自定义 ByteArrayOutputStream 来捕获编码后的数据
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(baos, imageWidth, imageHeight);
                    // 设置输出格式为 H.264
                    recorder.setFormat("h264");
                    // 设置视频编解码器
                    recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
                    // 设置比特率为 2 Mbps
                    recorder.setVideoBitrate(2000000); // 可以根据需要调整比特率
                    // 设置帧率
                    recorder.setFrameRate(frameRate);
                    // 设置 GOP 大小
                    recorder.setGopSize(10);
                    // 设置像素格式
                    recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
                    // 不使用交织模式
                    recorder.setInterleaved(false);
                    // 设置 FFmpeg 日志级别
                    recorder.setOption("loglevel", "error");

                    // 开始录制
                    recorder.start();

                    // 创建并启动捕获帧线程
                    Thread captureThread = createCaptureThread(grabber, recorder, baos);

                    // 创建并启动发送帧数据线程
                    Thread sendThread = createSendThread(outputStream);

                    // 启动所有线程
                    captureThread.start();
                    sendThread.start();

                    // 等待所有线程结束
                    captureThread.join();
                    sendThread.join();
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建一个捕获帧的线程。
     *
     * @param grabber  OpenCVFrameGrabber 实例,用于从摄像头捕获帧
     * @param recorder FFmpegFrameRecorder 实例,用于将帧编码为H.264格式
     * @param baos     用于捕获编码后数据的 ByteArrayOutputStream
     * @return 捕获帧的线程
     */
    private static Thread createCaptureThread(OpenCVFrameGrabber grabber, FFmpegFrameRecorder recorder, ByteArrayOutputStream baos) {
        return new Thread(() -> {
            while (true) {
                try {
                    Frame frame = grabber.grab();
                    if (frame == null || frame.image == null || frame.imageWidth <= 0 || frame.imageHeight <= 0) {
                        System.err.println("未能获取有效帧。帧详细信息:图像=" + frame + ", 图像宽度=" + frame.imageWidth + ", 图像高度=" + frame.imageHeight);
                        continue;
                    }

                    // 将帧写入 FFmpegFrameRecorder 进行编码
                    recorder.record(frame);

                    // 获取编码后的数据
                    byte[] data = baos.toByteArray();
                    if (data.length > 0) {
                        // 将帧数据放入队列
                        frameDataQueue.put(data);
                    }

                    // 重置 ByteArrayOutputStream 以便下次使用
                    baos.reset();
                } catch (Exception e) {
                    e.printStackTrace();
                    System.err.println("在捕获帧时发生错误: " + e.getMessage());
                    break;
                }
            }
        });
    }

    /**
     * 创建一个发送帧数据的线程。
     *
     * @param outputStream 输出流,用于向客户端发送帧数据
     * @return 发送帧数据的线程
     */
    private static Thread createSendThread(OutputStream outputStream) {
        return new Thread(() -> {
            long startTime = System.currentTimeMillis();
            long totalBytesSent = 0;
            while (true) {
                try {
                    // 从队列中获取帧数据
                    byte[] data = frameDataQueue.take();

                    // 发送帧大小(4字节整数)
                    int size = data.length;
                    outputStream.write(ByteBuffer.allocate(4).putInt(size).array());

                    // 发送帧数据
                    outputStream.write(data);
                    // 刷新输出流
                    outputStream.flush();

                    // 更新总发送数据量
                    totalBytesSent += size;

                    // 打印每次发送的帧大小
                    System.out.println("已发送帧大小: " + size + " 字节");

                    // 检查是否已经过了一秒
                    long currentTime = System.currentTimeMillis();
                    if (currentTime - startTime >= 1000) {
                        // 打印每秒发送的数据量
                        System.out.println("每秒发送数据量: " + totalBytesSent / 1024.0 + " KB");

                        // 重置计数器
                        totalBytesSent = 0;
                        startTime = currentTime;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
    }
}

CameraClient从服务器接收编码后的视频数据,解码接收到的数据为图像帧,并显示解码后的图像帧。

import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 一个用于接收 H.264 流并通过 CanvasFrame 显示的客户端。
 */
public class CameraClient {
    // 线程池大小
    private static final int THREAD_POOL_SIZE = 2;
    // 图像队列,用于存放从服务器接收到的图像
    private static final LinkedBlockingQueue<BufferedImage> imageQueue = new LinkedBlockingQueue<>(100);

    public static void main(String[] args) {
        try (Socket socket = new Socket("192.168.1.7", 8088);
             InputStream inputStream = socket.getInputStream()) {

            // 创建窗口
            CanvasFrame canvas = new CanvasFrame("Remote Screen");
            canvas.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);

            // 获取默认屏幕设备
            GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            int screenWidth = gd.getDisplayMode().getWidth();
            int screenHeight = gd.getDisplayMode().getHeight();

            // 设置CanvasFrame的大小为屏幕分辨率
            canvas.setSize(screenWidth, screenHeight);

            // 将CanvasFrame放置于屏幕中央(如果未全屏)
            canvas.setLocationRelativeTo(null);

            // 设置CanvasFrame为全屏模式
            if (gd.isFullScreenSupported()) {
                // 确保CanvasFrame是可见的
                canvas.setVisible(true);
                gd.setFullScreenWindow(canvas);
            } else {
                System.out.println("全屏模式不被支持");
            }

            // 创建线程池
            ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

            // 初始化 Java2DFrameConverter
            Java2DFrameConverter converter = new Java2DFrameConverter();

            // 开始接收帧数据
            Thread receiverThread = createReceiverThread(inputStream, executor, converter);

            // 开始显示帧
            Thread displayThread = createDisplayThread(canvas);

            // 启动接收和显示线程
            receiverThread.start();
            displayThread.start();

            // 等待所有线程结束
            receiverThread.join();
            displayThread.join();

            // 关闭线程池
            executor.shutdown();
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 创建一个接收帧数据的线程。
     *
     * @param inputStream 输入流
     * @param executor    线程池
     * @param converter   Java2DFrameConverter 实例
     * @return 接收帧数据的线程
     */
    private static Thread createReceiverThread(InputStream inputStream, ExecutorService executor, Java2DFrameConverter converter) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 读取帧大小(4字节整数)
                    byte[] sizeBytes = new byte[4];
                    int bytesRead = 0;
                    while (bytesRead < 4) {
                        int read = inputStream.read(sizeBytes, bytesRead, 4 - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 解析帧大小
                    int size = ByteBuffer.wrap(sizeBytes).asIntBuffer().get();

                    // 检查 size 是否为负数或异常大
                    if (size < 0 || size > 10 * 1024 * 1024) {
                        System.err.println("无效的帧大小: " + size);
                        continue;
                    }

                    System.out.println("接收到了帧大小: " + size + "字节");

                    // 读取帧数据
                    byte[] data = new byte[size];
                    bytesRead = 0;
                    while (bytesRead < size) {
                        int read = inputStream.read(data, bytesRead, size - bytesRead);
                        if (read == -1) {
                            throw new IOException("远程主机关闭了连接。");
                        }
                        bytesRead += read;
                    }

                    // 提交解码任务到线程池
                    executor.submit(() -> {
                        try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
                            FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(bais);
                            // 设置 FFmpeg 日志级别为 error,以减少日志输出
                            grabber.setOption("loglevel", "error");
                            // 设置输入流格式为 H.264
                            grabber.setFormat("h264");
                            // 增加分析时间和探测大小
                            grabber.setOption("analyzeduration", "5000000");
                            grabber.setOption("probesize", "5000000");
                            // 启动抓取器
                            grabber.start();

                            Frame frame;
                            while ((frame = grabber.grab()) != null) {
                                // 将帧转换为 BufferedImage
                                BufferedImage image = converter.getBufferedImage(frame);
                                // 将解码后的图像放入队列
                                imageQueue.put(image);
                            }
                        } catch (IOException | InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                } catch (IOException e) {
                    e.printStackTrace();
                    // 如果发生错误,则退出循环
                    break;
                }
            }
        });
    }

    /**
     * 创建一个显示帧的线程。
     *
     * @param canvas CanvasFrame 实例
     * @return 显示帧的线程
     */
    private static Thread createDisplayThread(CanvasFrame canvas) {
        return new Thread(() -> {
            while (true) {
                try {
                    // 从队列中获取图像
                    BufferedImage image = imageQueue.take();
                    // 显示帧
                    canvas.showImage(image);
                    // 控制每秒30帧的显示速率,1000毫秒 / 30 ≈ 33.3毫秒
                    canvas.waitKey(33);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

RealTimeVideoCapture实时视频捕获类,用于从摄像头捕获视频帧并保存为图片或视频文件。

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameGrabber;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

/**
 * 实时视频捕获类,用于从摄像头捕获视频帧并保存为图片或视频文件。
 */
public class RealTimeVideoCapture {

    public static void main(String[] args) {
        // 保存图片
        // saveImage();
        // 保存视频
        saveVideo();
    }

    /**
     * 从摄像头捕获视频帧并保存为图片。
     * 保存一百张图片后结束运行。
     */
    public static void saveImage() {
        // 初始化摄像头,0 表示默认摄像头
        OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
        try {
            grabber.start();
        } catch (Exception e) {
            System.err.println("无法启动摄像头: " + e.getMessage());
            return;
        }

        // 创建一个线程来处理视频流
        Thread thread = new Thread(() -> {
            int frameCount = 0;
            // 保存一百张图片后结束
            while (frameCount < 100) {
                try {
                    // 抓取一帧
                    Frame frame = grabber.grab();
                    if (frame == null) {
                        continue;
                    }

                    // 将帧转换为 BufferedImage
                    Java2DFrameConverter converter = new Java2DFrameConverter();
                    BufferedImage bufferedImage = converter.getBufferedImage(frame);

                    // 保存图片
                    String outputImagePath = "C:\\test\\frame_" + frameCount + ".jpg";
                    File outputImageFile = new File(outputImagePath);
                    ImageIO.write(bufferedImage, "jpg", outputImageFile);

                    // 打印保存路径
                    System.out.println("已保存: " + outputImagePath);

                    // 增加帧计数
                    frameCount++;
                } catch (Exception e) {
                    System.err.println("保存图片时发生错误: " + e.getMessage());
                }
            }
        });

        // 启动线程
        thread.setDaemon(true);
        thread.start();

        // 等待线程完成
        try {
            thread.join();
        } catch (InterruptedException e) {
            System.err.println("等待线程完成时发生错误: " + e.getMessage());
        }

        // 停止抓取器
        try {
            grabber.stop();
        } catch (Exception e) {
            System.err.println("停止摄像头时发生错误: " + e.getMessage());
        }
    }

    /**
     * 从摄像头捕获视频帧并保存为视频文件。
     * 保存一百秒视频后结束运行。
     */
    public static void saveVideo() {
        // 初始化摄像头,0 表示默认摄像头
        OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
        try {
            grabber.start();
        } catch (Exception e) {
            System.err.println("无法启动摄像头: " + e.getMessage());
            return;
        }

        // 获取视频参数
        int width = grabber.getImageWidth();
        int height = grabber.getImageHeight();
        double frameRate = grabber.getFrameRate();

        // 创建视频输出器
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(new File("C:\\test\\output_video.mp4"), width, height, 1);
        recorder.setFormat("mp4");
        recorder.setFrameRate(frameRate);
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        // 设置视频比特率为 1 Mbps
        recorder.setVideoBitrate(1000000);

        try {
            // 启动视频输出器
            recorder.start();

            // 创建一个线程来处理视频流
            Thread thread = new Thread(() -> {
                long startTime = System.currentTimeMillis();
                // 保存一百秒视频后结束
                while (System.currentTimeMillis() - startTime < 100000) {
                    try {
                        // 抓取一帧
                        Frame frame = grabber.grab();
                        if (frame == null) {
                            continue;
                        }

                        // 将帧写入视频文件
                        recorder.record(frame);
                    } catch (Exception e) {
                        System.err.println("录制视频时发生错误: " + e.getMessage());
                    }
                }
            });

            // 启动线程
            thread.setDaemon(true);
            thread.start();

            // 等待线程完成
            try {
                thread.join();
            } catch (InterruptedException e) {
                System.err.println("等待线程完成时发生错误: " + e.getMessage());
            }

            // 停止抓取器和记录器
            grabber.stop();
            recorder.stop();
        } catch (Exception e) {
            System.err.println("启动视频记录器时发生错误: " + e.getMessage());
        }
    }
}

2.3.格式转换

在视频和图像处理中,经常需要将一种格式的文件转换为另一种格式。以下我们将详细介绍如何使用 JavaCV 实现视频和图像的格式转换。videoFormatConverter 方法可以将输入的视频文件(例如 MP4 格式)转换为另一种格式(例如 AVI 格式),同时尽量保留原视频的所有配置(如分辨率、帧率、比特率等)。imageFormatConverter 方法可以将输入的 PNG 格式的图片文件转换为 JPG 格式。

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

/**
 * 格式转换工具类,用于视频和图片格式的转换。
 */
public class FormatConverter {

    public static void main(String[] args) {
        // 视频转换
        videoFormatConverter();
        // 图片转换
        imageFormatConverter();
    }

    /**
     * 将输入视频文件从一种格式转换为另一种格式,并打印转换进度。
     * 该方法会尽量保留原视频的所有配置。
     */
    public static void videoFormatConverter() {
        // 输入视频文件路径
        String inputVideoPath = "C:\\test\\烘焙面包.mp4";
        // 输出视频文件路径
        String outputVideoPath = "C:\\test\\烘焙面包.avi";

        // 初始化视频读取器
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputVideoPath);
        try {
            grabber.start();
        } catch (Exception e) {
            System.err.println("无法打开输入视频文件: " + e.getMessage());
            return;
        }

        // 获取视频参数
        int width = (int) grabber.getImageWidth();
        int height = (int) grabber.getImageHeight();
        double frameRate = grabber.getFrameRate();
        int videoBitrate = (int) grabber.getVideoBitrate(); // 获取视频比特率
        int audioChannels = grabber.getAudioChannels(); // 获取音频通道数
        int audioSampleRate = (int) grabber.getSampleRate(); // 获取音频采样率
        int audioBitrate = (int) grabber.getAudioBitrate(); // 获取音频比特率

        // 创建视频输出器
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputVideoPath, width, height, audioChannels);
        recorder.setFormat("avi"); // 设置输出格式为 AVI
        recorder.setFrameRate(frameRate); // 设置帧率
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_MPEG4); // 设置视频编码器
        recorder.setVideoBitrate(videoBitrate); // 设置视频比特率
        if (audioChannels > 0) {
            recorder.setAudioCodec(avcodec.AV_CODEC_ID_MP3); // 设置音频编码器
            recorder.setSampleRate(audioSampleRate); // 设置音频采样率
            recorder.setAudioBitrate(audioBitrate); // 设置音频比特率
        }

        try {
            // 启动视频输出器
            recorder.start();

            // 获取视频的总帧数
            long totalFrames = grabber.getLengthInFrames();

            // 读取并记录每一帧
            Frame frame;
            long processedFrames = 0;
            while ((frame = grabber.grab()) != null) {
                try {
                    recorder.record(frame);
                    processedFrames++;

                    // 计算并打印进度
                    double progress = (double) processedFrames / totalFrames * 100;
                    System.out.printf("转换进度: %.2f%%\r", progress);
                } catch (Exception e) {
                    System.err.println("录制视频时发生错误: " + e.getMessage());
                }
            }

            // 打印完成信息
            System.out.println("转换完成: 100.00%");

            // 停止抓取器和记录器
            grabber.stop();
            recorder.stop();
        } catch (Exception e) {
            System.err.println("启动视频记录器时发生错误: " + e.getMessage());
        }
    }

    /**
     * 将输入图片文件从 PNG 转换为 JPG。
     */
    public static void imageFormatConverter() {
        // 输入图片文件路径
        String inputImagePath = "C:\\test\\测试图片.png";
        // 输出图片文件路径
        String outputImagePath = "C:\\test\\测试图片.jpg";

        try {
            // 读取输入图片
            BufferedImage inputImage = ImageIO.read(new File(inputImagePath));

            // 将图片保存为新的格式
            boolean success = ImageIO.write(inputImage, "jpg", new File(outputImagePath));
            if (success) {
                System.out.println("图片转换成功: " + outputImagePath);
            } else {
                System.err.println("图片转换失败");
            }
        } catch (IOException e) {
            System.err.println("读取或写入图片时发生错误: " + e.getMessage());
        }
    }
}
正文到此结束