1. 环境准备与项目搭建:你的第一个JavaCV项目

很多Java开发者一听到图像处理、视频分析,第一反应就是“这得用Python吧?”。我以前也是这么想的,总觉得OpenCV、FFmpeg这些库在Java生态里用起来会很别扭。但自从几年前在一个必须用Java栈的项目里硬着头皮用了JavaCV之后,我发现这玩意儿真香。它本质上是一个Java版的OpenCV和FFmpeg封装,让你能在熟悉的Java环境里,用几乎原生的性能去处理多媒体任务。今天我就把自己踩过的坑和总结出来的实战经验,掰开揉碎了分享给你。

首先,咱们得把项目架子搭起来。和原始文章里提到的一样,你不一定需要去OpenCV官网下载那一大堆东西。JavaCV的“platform”包是个神器,它把常见的本地库都打包好了,跨平台问题它帮你解决。我推荐直接用Maven,这是最省心的方式。在你的pom.xml里加入下面这个依赖:

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.9</version>
</dependency>

这个版本号你可以去Maven中央仓库查最新的,我写文章时1.5.9是稳定版。这个包有点大,因为它包含了Windows、Linux、macOS等多个平台的本地库,第一次下载会慢一点,但一劳永逸。如果你特别在意项目体积,也可以只引入你需要的模块,比如javacv、opencv-platform、ffmpeg-platform,但我不建议新手这么干,容易缺库导致各种UnsatisfiedLinkError。

依赖加好,等Maven下载完,你的环境其实就准备好了百分之九十。剩下的百分之十是什么?是理解两个核心概念:Mat 和 Frame。你可以把Mat(矩阵)想象成OpenCV世界里处理图片的“万能容器”,一张图片进来,不管是JPG还是PNG,都会被转换成一个个数字组成的矩阵,亮度、颜色都藏在里面。而Frame是JavaCV里更通用的一个帧对象,它可以装图像数据、音频数据,或者别的什么,是从摄像头、视频文件里抓出来的原始单位。搞明白它俩,后面写代码就顺畅多了。

2. 图像处理入门:从美颜到人脸检测

咱们先玩点直观的——处理图片。这是检验环境是否搭成功的快速方法。

2.1 5分钟实现图片美颜效果

原始文章里给了美颜的代码,我用更直白的方式再解释一下。所谓美颜滤镜,很多其实就是一种叫“双边滤波”的算法。它像个聪明的修图师,能平滑皮肤(减少噪点),但同时又能保留眉毛、眼睛、嘴唇这些边缘的清晰度。直接上代码,我加了更详细的注释:

import org.bytedeco.opencv.opencv_core.Mat;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
import static org.bytedeco.opencv.global.opencv_imgproc.bilateralFilter;

public class SimpleBeautify {
    public static void main(String[] args) {
        // 1. 读取图片:把硬盘上的图片文件加载成一个Mat矩阵
        // 路径换成你自己的,支持jpg, png等常见格式
        Mat sourceImage = imread("C:/selfies/my_photo.jpg");

        // 2. 准备一个“空画布”,用来存放美颜后的结果
        Mat beautifiedImage = new Mat();

        // 3. 核心魔法:调用双边滤波函数
        // 这几个参数是关键:
        // - sourceImage: 原图
        // - beautifiedImage: 结果存放处
        // - 15: 颜色空间的滤波强度。值越大,磨皮效果越强,但别超过20,不然就变塑料娃娃了。
        // - 30: 坐标空间的滤波强度。通常是上一个值的两倍,决定考虑多远的像素来平滑。
        // - 7: 像素值差异的阈值。这个值小一点,边缘保留得更好。
        bilateralFilter(sourceImage, beautifiedImage, 15, 30, 7);

        // 4. 把处理好的矩阵,再保存成图片文件
        imwrite("C:/selfies/my_photo_beautified.jpg", beautifiedImage);
        System.out.println("美颜完成!快去看看吧。");
    }
}

运行这段代码,如果没报错并且生成了新图片,恭喜你,JavaCV的大门已经打开了。你可以调整15、30、7这几个参数,感受一下不同数值带来的效果变化,这是理解算法最直接的方式。

2.2 人脸检测:给你的照片画上“智能框框”

人脸检测是很多酷炫应用(如人脸识别、贴纸AR)的第一步。JavaCV做这个非常方便,因为它内置了OpenCV那些训练好的级联分类器。原始文章里提到了haarcascade_frontalface_alt.xml这个文件,你不需要自己去OpenCV官网下载整个安装包。更简单的方法是:直接用JavaCV自带的。这些文件通常被打包在jar包里,我们可以通过ClassLoader读取。我改进了一个更健壮、不依赖外部文件路径的版本:

import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
import java.net.URL;
import static org.bytedeco.opencv.global.opencv_imgcodecs.*;
import static org.bytedeco.opencv.global.opencv_imgproc.*;

public class RobustFaceDetector {
    public static void main(String[] args) throws Exception {
        Mat image = imread("C:/photos/group_photo.jpg");

        // 关键一步:从JavaCV资源中加载人脸检测模型
        // 这个路径是jar包内的,直接写死就行,跨平台通用
        URL cascadeUrl = ClassLoader.getSystemResource("haarcascade_frontalface_alt.xml");
        if (cascadeUrl == null) {
            // 如果上述方式找不到,尝试另一种常见打包路径
            cascadeUrl = ClassLoader.getSystemResource("org/bytedeco/opencv/data/haarcascade_frontalface_alt.xml");
        }
        CascadeClassifier faceDetector = new CascadeClassifier(cascadeUrl.getFile());

        RectVector faceDetections = new RectVector();
        // 执行检测!所有找到的人脸矩形框会存入faceDetections
        faceDetector.detectMultiScale(image, faceDetections);

        System.out.println("发现了 " + faceDetections.size() + " 张脸");
        // 给每张脸画个绿色框框
        Scalar greenColor = new Scalar(0, 255, 0, 0); // BGR格式,绿色
        for (Rect rect : faceDetections.get()) {
            rectangle(image,
                      new Point(rect.x(), rect.y()),
                      new Point(rect.x() + rect.width(), rect.y() + rect.height()),
                      greenColor,
                      3, // 线宽
                      LINE_AA, // 抗锯齿的线型,比LINE_8看起来更平滑
                      0);
        }

        imwrite("C:/photos/group_photo_with_faces.jpg", image);
    }
}

这个方法的好处是项目部署时,不用担心服务器上没有那个XML文件。人脸检测的精度受图片质量、光线、人脸角度影响很大。如果发现漏检,可以尝试换用其他模型文件,比如haarcascade_frontalface_default.xml或者更精确的lbpcascade_frontalface.xml,加载方式都一样。

3. 音视频处理实战:剪辑、提取与录屏

处理完静态图片,咱们升级一下,玩转音视频。这才是JavaCV真正发挥威力的地方,因为它背后站着FFmpeg这位“多媒体领域的瑞士军刀”。

3.1 从视频中“剥离”出纯净音频

有时候我们只需要视频里的背景音乐或对话,保存成MP3。用JavaCV做这个,比用各种格式转换软件更可控,还能集成到你的自动化流程里。原始文章的代码稍显复杂,我把它重构得更清晰,并加上了资源关闭的安全处理:

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

public class AudioExtractor {
    public static void extractAudio(String videoPath, String outputAudioPath) {
        FFmpegFrameGrabber grabber = null;
        FFmpegFrameRecorder recorder = null;
        try {
            grabber = new FFmpegFrameGrabber(videoPath);
            grabber.start(); // 启动抓取器,解析视频头信息

            // 基于视频源的信息,创建音频录制器
            recorder = new FFmpegFrameRecorder(outputAudioPath,
                                                grabber.getAudioChannels());
            recorder.setFormat("mp3");
            recorder.setSampleRate(grabber.getSampleRate());
            recorder.start();

            Frame frame;
            System.out.println("开始提取音频...");
            while ((frame = grabber.grab()) != null) {
                // 只处理音频帧(frame.samples不为空)
                if (frame.samples != null) {
                    recorder.record(frame);
                }
                // 视频帧就忽略掉了
            }
            System.out.println("音频提取完成!");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 确保资源被关闭,避免内存泄漏
            try { if (recorder != null) recorder.close(); } catch (Exception e) {}
            try { if (grabber != null) grabber.close(); } catch (Exception e) {}
        }
    }

    public static void main(String[] args) {
        extractAudio("D:/videos/my_vlog.mp4", "D:/audio/extracted_bgm.mp3");
    }
}

3.2 不依赖外部FFmpeg:纯JavaCV实现视频剪辑

原始文章里调用本地ffmpeg.exe来剪辑,这在某些受限制的服务器环境可能不行。其实JavaCV的FFmpegFrameGrabber和FFmpegFrameRecorder本身就能完成剪辑。思路是:定位到开始时间,然后只抓取和录制我们需要的片段。下面是一个剪辑视频的例子:

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

public class PureJavaVideoClipper {
    public static void clipVideo(String inputPath, String outputPath,
                                 double startSecond, double durationSecond) throws Exception {
        try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputPath);
             FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputPath,
                                                                     grabber.getImageWidth(),
                                                                     grabber.getImageHeight())) {

            grabber.start();
            // 配置录制器,复制原视频的参数
            recorder.setFormat(grabber.getFormat());
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setVideoCodec(grabber.getVideoCodec());
            recorder.setAudioCodec(grabber.getAudioCodec());
            recorder.start();

            int startFrame = (int)(startSecond * grabber.getFrameRate());
            int totalFramesToGrab = (int)(durationSecond * grabber.getFrameRate());

            // 跳过开始部分:逐帧grab,但不记录,直到到达起始帧
            for (int i = 0; i < startFrame; i++) {
                if (grabber.grab() == null) break;
            }

            // 抓取并录制所需片段
            for (int i = 0; i < totalFramesToGrab; i++) {
                Frame frame = grabber.grab();
                if (frame == null) break;
                recorder.record(frame);
            }
            // try-with-resources会自动关闭grabber和recorder
        }
        System.out.println("视频剪辑完成,输出至: " + outputPath);
    }
}

这个方法虽然逻辑清晰,但对于长视频跳转会有点慢,因为它是线性跳帧。对于精确到秒级的剪辑,这种方法完全够用。如果你需要处理大量剪辑任务,可以考虑更底层的FFmpeg命令包装,但上述纯Java方案在集成度和环境一致性上优势明显。

3.3 实现桌面录屏功能

录屏是一个很有趣的功能,可以用来做操作演示、游戏录制等。JavaCV结合Java的Robot类可以轻松实现。原始文章的代码已经很好,我补充一些关键参数说明和优化点:

import org.bytedeco.javacv.*;
import java.awt.*;

public class DesktopRecorder {
    public static void record(String outputFile, int recordSeconds) throws Exception {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        // 1. 创建录制器
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(
                outputFile, screenSize.width, screenSize.height);
        // 关键参数设置,影响输出文件大小和清晰度:
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); // 最通用的视频编码
        recorder.setFormat("mp4");
        recorder.setFrameRate(25); // 帧率,25或30比较流畅
        recorder.setVideoQuality(10); // 范围0-51,0为无损,23是默认值,值越小质量越高文件越大
        recorder.setVideoBitrate(1000000); // 比特率,1000000约等于1Mbps,清晰度不错
        recorder.start();

        // 2. 创建Java的机器人,用于截屏
        Robot robot = new Robot();
        Java2DFrameConverter converter = new Java2DFrameConverter();

        long startTime = System.currentTimeMillis();
        long endTime = startTime + (recordSeconds * 1000);

        System.out.println("开始录制桌面...");
        while (System.currentTimeMillis() < endTime) {
            // 截取全屏
            Frame frame = converter.convert(robot.createScreenCapture(
                new Rectangle(screenSize)));
            recorder.setTimestamp(1000L * (System.currentTimeMillis() - startTime));
            recorder.record(frame);
            // 根据帧率计算等待时间,避免CPU占用过高
            Thread.sleep(1000 / recorder.getFrameRate());
        }
        recorder.close();
        System.out.println("录制结束,文件保存在: " + outputFile);
    }
}

注意:录屏会占用显著的CPU资源,尤其是高分辨率和高帧率下。在实际项目中,你可能需要根据机器性能调整帧率、比特率,甚至考虑使用硬件加速(如指定recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264_QSV)来使用Intel核显编码,但这需要FFmpeg编译时支持相应特性)。

4. 流媒体开发:推流、拉流与摄像头应用

流媒体是当前的热门,JavaCV在这方面同样能打。你可以用它搭建简单的直播推流端,或者处理IP摄像头流。

4.1 搭建你的第一个直播推流服务器

原始文章使用了SRS(Simple RTMP Server)作为流媒体服务器,这是一个非常优秀的选择。咱们先把它跑起来。用Docker是最快的方式,如果你没有Docker,可以去SRS官网下载源码编译。

# 使用Docker一键启动SRS服务器
docker run -d -p 1935:1935 -p 1985:1985 -p 8080:8080 --name srs ossrs/srs:latest

这条命令启动了SRS,并映射了三个端口:1935是RTMP推流/拉流端口,8080是HTTP-FLV和HLS的播放端口,1985是管理API端口。启动后,你可以访问 http://你的服务器IP:8080 看到SRS的欢迎页面。

接下来,我们用JavaCV写一个推流客户端,把本地视频文件推送到这个SRS服务器:

import org.bytedeco.javacv.*;

public class StreamPusher {
    public static void pushMediaFileToStream(String inputFile, String rtmpUrl) throws Exception {
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
        grabber.start();

        // 从抓取器获取视频参数,用于初始化录制器(推流器)
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(rtmpUrl,
                grabber.getImageWidth(),
                grabber.getImageHeight(),
                grabber.getAudioChannels());

        // 推流到RTMP服务器,通常使用flv格式和h264编码
        recorder.setFormat("flv");
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        recorder.setFrameRate(grabber.getFrameRate());
        // 设置GOP大小,影响seek和网络适应性,一般设为帧率的2倍
        recorder.setGopSize((int)(grabber.getFrameRate() * 2));
        // 开启可变比特率,对网络更友好
        recorder.setVideoOption("tune", "zerolatency");
        recorder.setVideoOption("preset", "veryfast");
        recorder.start();

        Frame frame;
        while ((frame = grabber.grab()) != null) {
            recorder.record(frame);
        }
        recorder.close();
        grabber.close();
        System.out.println("推流结束");
    }

    public static void main(String[] args) throws Exception {
        // 推流地址格式:rtmp://服务器IP:1935/live/流名称
        // ‘live’是SRS的默认Vhost,'myStream'是你自定义的流密钥
        pushMediaFileToStream("D:/videos/test.mp4",
                              "rtmp://192.168.1.100:1935/live/myStream");
    }
}

运行这个推流代码后,你就可以用VLC播放器来验证了。打开VLC,点击“媒体” -> “打开网络串流”,输入拉流地址 rtmp://192.168.1.100:1935/live/myStream 或者 http://192.168.1.100:8080/live/myStream.flv,应该就能看到视频了。

4.2 摄像头实时采集与处理

把摄像头画面接入Java程序,能玩出很多花样。原始文章提供了一个很好的抽象基类AbstractCameraApplication,我们基于它来实现几个具体功能。首先,确保你的摄像头已经连接好。如果是笔记本电脑,通常索引为0。

实时保存摄像头视频到MP4文件:

import org.bytedeco.javacv.*;
import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avutil.*;

public class CameraToMp4 extends AbstractCameraApplication {
    private FFmpegFrameRecorder recorder;

    @Override
    protected void initOutput() throws Exception {
        // 创建一个录制器,将输出保存为mp4文件
        recorder = new FFmpegFrameRecorder("camera_record.mp4",
                                            getCameraImageWidth(),
                                            getCameraImageHeight());
        recorder.setFormat("mp4");
        recorder.setVideoCodec(AV_CODEC_ID_H264);
        recorder.setPixelFormat(AV_PIX_FMT_YUV420P); // 最通用的像素格式
        recorder.setFrameRate(getFrameRate());
        recorder.setVideoQuality(0); // 较高画质
        recorder.start();
    }

    @Override
    protected void output(Frame frame) throws Exception {
        // 将每一帧画面写入录制器
        recorder.record(frame);
    }

    @Override
    protected void releaseOutputResource() throws Exception {
        if (recorder != null) {
            recorder.close();
        }
    }

    public static void main(String[] args) {
        // 启动应用,录制15秒
        new CameraToMp4().action(15);
    }
}

定时从摄像头抓图:

这个功能可以用在简易的安防监控、打卡拍照等场景。我们修改基类的抓取间隔,实现每秒抓一张图。

import org.bytedeco.javacv.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

public class CameraSnapshot extends AbstractCameraApplication {
    private Java2DFrameConverter converter = new Java2DFrameConverter();
    private int snapshotCount = 0;

    @Override
    protected void initOutput() throws Exception {
        // 不需要初始化特殊的输出器,直接准备文件目录即可
        File dir = new File("snapshots");
        if (!dir.exists()) dir.mkdirs();
    }

    @Override
    protected void output(Frame frame) throws Exception {
        // 将帧转换为BufferedImage并保存为JPEG
        BufferedImage image = converter.getBufferedImage(frame);
        File outputFile = new File(String.format("snapshots/snapshot_%03d.jpg", ++snapshotCount));
        ImageIO.write(image, "jpg", outputFile);
        System.out.println("已保存截图: " + outputFile.getName());
    }

    @Override
    protected void releaseOutputResource() throws Exception {
        // 无特殊资源需要释放
    }

    @Override
    protected int getInterval() {
        // 设置抓图间隔为1000毫秒(1秒)
        return 1000;
    }

    public static void main(String[] args) {
        // 运行30秒,预计会抓取30张图片
        new CameraSnapshot().action(30);
    }
}

运行这个程序,你会在项目根目录下看到一个snapshots文件夹,里面按秒生成了摄像头截图。你可以轻松地修改getInterval()方法,实现每分钟抓图,或者结合运动检测算法,只在画面有变化时才保存图片,这样就更实用了。

5. 进阶实战:训练一个简易的人脸识别模型

前面我们做了人脸检测(找到人脸在哪),现在玩点更智能的——人脸识别(判断这是谁的脸)。这是一个机器学习任务,JavaCV通过封装OpenCV的face模块,让我们能在Java里完成模型的训练和预测。

5.1 准备训练数据:图片收集与整理

模型训练的第一步,也是最重要的一步,是准备数据。你需要为每个要识别的人建立一个文件夹,里面放上他/她的多张人脸照片。比如:

  • dataset/zhangsan/ 里面放10-20张张三的照片
  • dataset/lisi/ 里面放10-20张李四的照片

照片要求:

  1. 最好是正脸,表情自然,光线均匀。
  2. 背景尽量简单,减少干扰。
  3. 图片格式可以是JPG、PNG。
  4. 每类(每个人)的图片数量尽量均衡。

我建议一开始就用两个人来测试,比如用你自己和一个朋友的照片,各准备15张左右。把这些图片按上述结构放好。

5.2 训练人脸识别模型

接下来,我们写一个训练程序。这里我使用LBPHFaceRecognizer,它对光照变化相对更鲁棒一些,而且训练速度快。

import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_face.LBPHFaceRecognizer;
import java.io.File;
import java.util.*;

public class FaceModelTrainer {
    public static void main(String[] args) {
        // 1. 定义数据集路径和标签
        String trainingDatasetDir = "C:/face_dataset/";
        Map<String, Integer> labelMap = new HashMap<>();
        labelMap.put("zhangsan", 1); // 给张三分配标签1
        labelMap.put("lisi", 2);     // 给李四分配标签2

        // 用于存放所有图片矩阵和对应标签
        List<Mat> images = new ArrayList<>();
        List<Integer> labels = new ArrayList<>();

        // 2. 遍历数据集,读取并预处理每张图片
        for (Map.Entry<String, Integer> entry : labelMap.entrySet()) {
            String personName = entry.getKey();
            int label = entry.getValue();
            File personDir = new File(trainingDatasetDir + personName);

            for (File imageFile : personDir.listFiles()) {
                if (!imageFile.getName().toLowerCase().endsWith(".jpg")) continue;

                // 读取图片
                Mat img = opencv_imgcodecs.imread(imageFile.getAbsolutePath());
                if (img.empty()) {
                    System.out.println("无法读取图片: " + imageFile);
                    continue;
                }

                // 转换为灰度图(人脸识别通常在灰度图上进行)
                Mat grayImg = new Mat();
                opencv_imgproc.cvtColor(img, grayImg, opencv_imgproc.COLOR_BGR2GRAY);

                // 可选的:调整图片到统一尺寸,例如100x100像素
                Mat resizedImg = new Mat();
                opencv_imgproc.resize(grayImg, resizedImg, new Size(100, 100));

                images.add(resizedImg);
                labels.add(label);
                System.out.println("已加载: " + imageFile + " -> 标签" + label);
            }
        }

        // 3. 将List转换为训练所需的格式
        MatVector imagesVector = new MatVector(images.toArray(new Mat[0]));
        Mat labelsMat = new Mat(labels.size(), 1, opencv_core.CV_32SC1);
        for (int i = 0; i < labels.size(); i++) {
            labelsMat.put(i, 0, labels.get(i));
        }

        // 4. 创建识别器并训练
        LBPHFaceRecognizer recognizer = LBPHFaceRecognizer.create();
        System.out.println("开始训练模型,共" + images.size() + "张图片...");
        recognizer.train(imagesVector, labelsMat);

        // 5. 保存训练好的模型
        recognizer.save("face_recognizer_model.yml");
        recognizer.close();
        System.out.println("模型训练完成,已保存为 'face_recognizer_model.yml'");
    }
}

运行这段代码,如果一切顺利,你会得到一个face_recognizer_model.yml文件。这就是我们训练好的人脸识别模型。

5.3 使用模型进行实时人脸识别预测

模型有了,现在我们来写一个程序,用摄像头实时检测人脸,并用模型识别是谁。这其实就是很多门禁系统、打卡系统的核心。

import org.bytedeco.javacv.*;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
import org.bytedeco.opencv.opencv_face.LBPHFaceRecognizer;
import javax.swing.*;
import java.awt.image.BufferedImage;

import static org.bytedeco.opencv.global.opencv_imgproc.*;

public class RealTimeFaceRecognition {
    public static void main(String[] args) throws Exception {
        // 1. 加载我们训练好的模型
        LBPHFaceRecognizer recognizer = LBPHFaceRecognizer.create();
        recognizer.read("face_recognizer_model.yml");

        // 2. 加载人脸检测器(和之前人脸检测用的一样)
        CascadeClassifier faceDetector = new CascadeClassifier(
                ClassLoader.getSystemResource("haarcascade_frontalface_alt.xml").getFile());

        // 3. 打开摄像头
        OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
        grabber.setImageWidth(640);
        grabber.setImageHeight(480);
        grabber.start();

        // 4. 创建一个窗口用于显示
        CanvasFrame canvas = new CanvasFrame("实时人脸识别");
        canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
        Java2DFrameConverter imgConverter = new Java2DFrameConverter();

        System.out.println("开始实时识别,按窗口关闭按钮退出...");
        while (canvas.isVisible()) {
            // 抓取一帧
            Frame frame = grabber.grab();
            if (frame == null) continue;

            Mat colorImage = converter.convert(frame);
            Mat grayImage = new Mat();
            cvtColor(colorImage, grayImage, COLOR_BGR2GRAY);
            equalizeHist(grayImage, grayImage); // 直方图均衡化,提升检测率

            // 检测人脸
            RectVector faces = new RectVector();
            faceDetector.detectMultiScale(grayImage, faces);

            // 遍历每个检测到的人脸
            for (int i = 0; i < faces.size(); i++) {
                Rect rect = faces.get(i);
                // 在原图上画矩形框
                rectangle(colorImage,
                          new Point(rect.x(), rect.y()),
                          new Point(rect.x() + rect.width(), rect.y() + rect.height()),
                          new Scalar(0, 255, 0, 0), 2, LINE_AA, 0);

                // 截取人脸区域,并预处理(调整大小、保持灰度)
                Mat faceROI = new Mat(grayImage, rect);
                Mat resizedFace = new Mat();
                resize(faceROI, resizedFace, new Size(100, 100)); // 必须和训练时尺寸一致!

                // 使用模型进行预测
                int[] predictedLabel = new int[1];
                double[] confidence = new double[1];
                recognizer.predict(resizedFace, predictedLabel, confidence);

                // 在矩形框上方显示识别结果
                String labelText;
                if (confidence[0] < 70) { // 置信度阈值,低于这个值才认为是可信识别
                    labelText = (predictedLabel[0] == 1) ? "张三" : "李四";
                } else {
                    labelText = "陌生人";
                }
                String info = String.format("%s (%.1f)", labelText, confidence[0]);
                putText(colorImage, info,
                        new Point(rect.x(), rect.y() - 10),
                        FONT_HERSHEY_SIMPLEX, 0.6, new Scalar(255, 0, 0, 0), 2);
            }

            // 将处理后的Mat显示到窗口
            Frame processedFrame = converter.convert(colorImage);
            canvas.showImage(processedFrame);

            // 短暂延迟,控制刷新率
            Thread.sleep(30);
        }
        grabber.close();
        recognizer.close();
    }
}

运行这个程序,把你的脸对准摄像头,看看它能不能正确地把你识别为“张三”或“李四”。confidence值表示预测的置信度,值越低表示越确信。你可以调整代码中的置信度阈值(我设的70),来平衡识别率和误识率。

这里我用了LBPHFaceRecognizer,你也可以尝试OpenCV提供的其他模型,比如EigenFaceRecognizer或FisherFaceRecognizer,只需在训练和预测时替换对应的类即可。不同的模型对数据的要求和识别特性略有不同,多试试能找到最适合你场景的那个。

从环境搭建到图像处理,再到音视频剪辑和流媒体推流,最后到训练自己的人脸识别模型,我们一路走下来,其实可以看到JavaCV的能力边界非常广。它绝不是OpenCV或FFmpeg的一个简单移植,而是一个让Java开发者能够深入多媒体处理领域的坚实桥梁。我最初用它只是为了解决项目里的一个特定需求,但后来发现,有了它,很多之前觉得必须用Python或C++才能做的功能,在Java里也能优雅地实现。当然,它也不是万能的,极致的性能优化或者最新的算法模型,可能还是需要回归到原生库。但对于绝大多数应用场景,从简单的图片滤镜到复杂的实时视频分析管道,JavaCV都能提供稳定、高效的解决方案。最关键的是,它让你能继续留在熟悉的Java生态里,利用现有的工具链和部署经验,这是非常大的优势。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐