JavaCV入门教程

转载自:https://www.duheart.cn/


JavaCV是计算机视觉领域的开发人员(OpenCV、FFmpeg、libdc1394、PGR FlyCapture、OpenKinect、li.lsense、CL PS3 Eye Driver、videoInput、ARToolKitPlus、flandmark、Leptonica和Tesseract)常用库的JavaCPP预置的包装器,并提供实用的程序类使它们的功能更容易在Java平台上使用,包括Android。

JavaCV还提供了硬件加速的全屏图像显示(CanvasFrame和GLCanvasFrame)、在多核(并行)上并行执行代码的简便方法、照相机和投影机的用户友好的几何和颜色校准(GeometricCalibrator,ProCamometricCalibrato)r,ProCamColorCalibrator),特征点的检测和匹配(ObjectFinder),一组用于实现投影仪-照相机系统的直接图像对齐的类(主要是GNImageAligner、ProjectiveTransformer、ProjectiveColorTransformer、ProCamTransformer和ReflectanceInitializer),一个blob分析包(BLUB),以及JavaCV类中的各种功能。其中一些类还具有OpenCL和OpenGL的对应类,它们的名称以CL结尾或以GL开始,即:JavaCVCL、GLCanvasFrame等。

一、依赖下载

Maven依赖

<!-- 仅JavaCV -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv-platform</artifactId>
    <version>1.5.4</version>
</dependency>

Gradle依赖

dependencies {
    compile group: 'org.bytedeco', name: 'javacv-platform', version: '1.5.4'
}

二、什么是JavaCPP以及JavaCV是如何通过JavaCPP封装了FFmpeg的音视频操作?

大家知道FFmpeg是C语言中著名的音视频库(注意,不是c++。使用c++调用ffmpeg库的性能损失与Java方式调用损耗相差并不大)。

JavaCV利用JavaCPP在FFmpeg和Java之间构建了桥梁,我们通过这个桥梁可以方便的调用FFmpeg,当然这并不是没有损失的,性能损失暂且不提,最主要问题在于调用ffmpeg之于jvm是native方法,所以通过ffmpeg创建的结构体实例与常量、方法等等都是使用堆外内存,都需要像C那样手动的释放这些资源(jvm并不会帮你回收这部分),以此来保证不会发生内存溢出/泄露等风险。

Javapp在Java内部提供了对本地C++的高效访问,这与一些C/C++编译器与汇编语言交互的方式不同。不需要发明新的语言,比如SWIG、SIP、C++、CLI、Cython或Rython。相反,类似于CPpyy为Python所做的努力,它利用了Java和C++之间的语法和语义相似性。在引擎盖下,它使用JNI,因此除了Java、SE和RoboVM(指令)之外,它还适用于Java SE的所有实现。

JavaCV的大体结构如下:

JavaCV通过JavaCPP调用了FFmpeg,并且对FFmpeg复杂的操作进行了封装,把视频处理分成了两大类:“帧抓取器”(FrameGrabber)和“帧录制器”(又叫“帧推流器”,FrameRecorder)以及用于存放音视频帧的Frame。

整体结构如下:

视频源—->帧抓取器(FrameGabber) —->抓取视频帧(Frame)—->帧录制器(FrameRecorder)—->推流/录制—->流媒体服务/录像文件

1、帧抓取器(FrameGrabber

封装了FFmpeg的检索流信息,自动猜测视频解码格式,音视频解码等具体API,并把解码完的像素数据(可配置像素格式)或音频数据保存到Frame中返回。

2、帧录制器/推流器(FrameRecorder

封装了FFmpeg的音视频编码操作和封装操作,把传参过来的Frame中的数据取出并进行编码、封装、发送等操作流程。

3、过滤器(FrameFilter

FrameFilter的实现类其实只有FFmpegFrameFilter,因为只有ffmpeg支持音视频的过滤器操作,主要封装了简单的ffmpeg filter操作。

3、转换工具(FrameConverter

FrameConverter封装了常用的转换操作,比如opencv与Frame的互转、java图像与Frame的互转以及安卓平台的Bitmap图像与Frame互转操作。

FrameConverter的子类: AndroidFrameConverterJava2DFrameConverterJavaFXFrameConverterLeptonicaFrameConverterOpenCVFrameConverter

3、图像预览工具(CanvasFrame

CanvasFrame是用于预览Frame图像的工具类,但是这个工具类的gama值通常是有问题的,所以显示的图像可能会偏色,但是不影响最终图像的色彩。

CanvasFrame内部是使用的swing的Canvas画板操作,使用canvas画板绘制图像。

4、Frame

用于存放解码后的视频图像像素和音频采样数据(如果没有配置FrameGrabber的像素格式和音频格式,那么默认解码后的视频格式是yuv420j,音频则是pcm采样数据)。

里面包含解码后的图像像素数据,大小(分辨率)、音频采样数据,音频采样率,音频通道(单声道、立体声等等)等等数据

Frame里面的一个字段opaque引用AVFrame、AVPacket、Mat等数据,也即是说,如果你是解码后获取的Frame,里面存放的属性找不到你需要的,可以从opaque属性中取需要的AVFrame原生属性。

例如:

Frame frame = grabber.grabImage();//获取视频解码后的图像像素,也就是说这时的Frame中的opaque存放的是AVFrame
AVFrame avframe=(AVFrame)frame.opaque;//把Frame直接强制转换为AVFrame
long lastPts=avframe.pts();
System.err.println("显示时间:"+lastPts);

FFmpeg中两个重要的结构体:AVPacketAVFrame。

AVPacket是ffmpeg中存放解复用(未解码)的音视频帧的结构体,视频只有可能是一帧,大小不定(分为关键帧/I帧、P帧和B帧三种视频帧)。AVPacket属性除了包含音视频帧以外,还包含:pts(显示时间)dts(解码时间)、duration(持续时长)、stream_index(表示音视频流的通道,音频和视频一般是分开的,通过stream_index来区分是视频帧还是音频帧)、flags(AV_PKT_FLAG_KEY(值是1):关键帧,2-损坏数据,4-丢弃数据)、pos(在流媒体中的位置)、size

某些情况下,使用AVPacket直接推流(不经过转码)的过程称之为:转封装。

AVFrame是ffmpeg中存放解码后的音视频图像像素或音频采样数据的结构体,大部分属性是与Frame相同的,多了像素格式、pts、dts和音频布局等等属性。

AVPakcet和AVFrame的使用流程如下图所示:

三、JavaCV是如何封装了opencv的音视频及图像处理操作?

与ffmpeg相似的是,JavaCV把opencv的操作也抽象成了“读取媒体文件或地址,循环抓取图像,停止”的流程,其中不同的是,opencv可以直接循环读取设备列表。JavaCV把opencv的操作分成了两大块:OpenCVFrameGrabber和OpenCVFrameRecorder。其中OpenCVFrameGrabber用来读取设备、视频流媒体和图片文件等,而OpenCVFrameRecorder则用来录制文件。

1、OpenCVFrameGrabber读取设备、媒体文件及流

OpenCVFrameGrabber其实内部封装了opencv的VideoCapture操作,支持设备、视频文件、图片文件和流媒体地址(rtsp/rtmp等)。

可以通过 ImageMode设置色彩模式,支持ImageMode.COLOR(色彩图)和ImageMode.GRAY(灰度图)

注意:opencv并不支持音频读取和录制等操作,只支持视频文件、视频流媒体、图像采集设备的画面抓取。

另外需要注意的是,读取非动态图片,只能读取一帧。

通过OpenCVFrameRecordergrab()抓取到的图像是Frame,其实javaCV内部通过OpenCVFrameConverter把opencv的Mat转换为了Frame,也即是说,Frame中可以直接获取Mat或者也可以通过OpenCVFrameConverter实现MatFrame的互转。

2、OpenCVFrameGrabber获取的Frame和Mat之间的关系

OpenCVFrameGrabber既然内部封装的是opencv的VideoCapture,那么获取的数据结构应该是Mat,通过OpenCVFrameGrabber源码也证实了这一点,那么我们如何直接获取Mat呢?

实际上在OpenCVFrameGrabber中把获取到的Mat转换为Frame,并且在Frame中使用opaque字段引用了Mat结构体。

在使用OpenCVFrameGrabber获取Frame的时候不需要再进行FrameMat转换,只需要使用

Mat img = (Mat)frame.opaque;

即可获得Mat结构体。

3、OpenCVFrameRecorder录制媒体文件

OpenCVFrameRecorder主要封装了opencv的VideoWriter模块,用来实现视频流媒体的录制操作,支持的格式同样参考:http://mp4ra.org/#/codecshttps://www.fourcc.org/codecs.php 这两个列表。

通过循环recordFrame就可以录制视频流媒体,当然如果是进行图像处理操作,得到的是Mat,就可以通过OpenCVFrameConverterMat转换成Frame即可进行record()录制操作。

4、OpenCVFrameConverter进行MatIplImageFrame的互转

由于我们使用opencv需要进行图像处理等操作,处理完得到的是Mat或者IplImage,读取和录制却是Frame,所以需要使用OpenCVFrameConverter提供的转换操作来完成两个对象间的转换操作。

注意:此转换操作实际上常用于FFmpeg获取到的视频图像转换为Mat,因为OpencvFrameGrabber本身获取到的Frame包含Mat的引用

IplImage与Frame互转

//把frame转换成IplImage
IplImage convertToIplImage(Frame frame)
//把IplImage转换成frame
Frame convert(IplImage img) 

Mat与Frame互转

//frame转换成Mat
Mat convertToMat(Frame frame)
//mat转换成frame
Frame convert(Mat mat)

四、帧录制器/推流器(FrameRecorder)的原理与应用

FrameRecorder用于音视频/图片的封装、编码、推流和录制保存等操作。把从FrameGrabber或者FrameFilter获取的Frame中的数据取出并进行编码、封装、推流发送等操作流程。为了方便理解和阅读,下文开始我们统一把FrameRecorder简称为:录制器

FrameRecorderFrameGrabber类似,也是个抽象类,抽象了所有录制器的通用方法和一些共用属性。

FrameRecorder只有两个子类实现:FFmpegFrameRecorderOpenCVFrameRecorder

1、两个FrameRecorder实现类的介绍

  • FFmpegFrameRecorder:使用ffmpeg作为音视频编码/图像、封装、推流、录制库。除了支持音视频录制推流之外,还可以支持gif/apng动态图录制,对于音视频这块的功能ffmpeg依然还是十分强大的保障。

  • OpenCVFrameRecorder:使用opencv的videowriter录制视频,不支持像素格式转换,根据opencv的官方介绍,如果在ffmpeg可用的情况下,opencv的视频功能可能也是基于ffmpeg的,macos下基于avfunctation。

总得来说,音视频这块还是首选 ffmpeg 的录制器。但是凡事都有例外,由于 javacv 的包实在太大,开发的时候下载依赖都要半天。为了减少程序的体积大小,如果只需要进行图像处理/图像那个识别的话只使用 opencv 的情况就能解决问题,那就 opencvFrameGrabberOpenCVFrameRecorder 配套使用吧,毕竟可以省很多空间。

同样的,如果只是做音视频流媒体,那么能使用ffmpeg解决就不要用其他库了,能节省一点空间是一点,确实javacv整个完整包太大了,快要1G了。

2、FrameRecorder的结构和流程

FrameRecorder的整个代码结构和运作流程很简单

初始化和设置参数—>start()—>循环record(Frame frame)—>close()

close()包含stop()release()两个操作,这个要注意。

由于只有两个实现类,就直接分开单独分析了

3、FFmpegFrameRecorder结构和分析

FFmpegFrameRecorder 是比较复杂的,我们主要它来实现像素格式转换、视频编码和录制推流。

我们把流程分为转封装流程和转码流程

转封装流程

FFmpegFrameRecorder初始化–>start()–>循环 recordPacket(AVPacket)–>close()

这里的AVPacket是未解码的解复用视频帧。

转码编码流程

FFmpegFrameRecorder初始化–>start()–>循环record(Frame)/recordImage()/recordSamples()–>close()

3、FFmpegFrameRecorder初始化及参数说明

FFmpegFrameRecorder初始化支持文件地址、流媒体推流地址、OutputStream流的形式和imageWidth(视频宽度)、imageHeight(图像高度)、audioChannels(音频通道,1-单声道,2-立体声)

FFmpegFrameRecorder recorder=new FFmpegFrameRecorder(output, width,height,0);
recorder.setVideoCodecName(videoCodecName);//优先级高于videoCodec
recorder.setVideoCodec(videoCodec);//只有在videoCodecName没有设置或者没有找到videoCodecName的情况下才会使用videoCodec
//recorder.setFormat(format);//只支持flv,mp4,3gp和avi四种格式,flv:AV_CODEC_ID_FLV1;mp4:AV_CODEC_ID_MPEG4;3gp:AV_CODEC_ID_H263;avi:AV_CODEC_ID_HUFFYUV;
recorder.setPixelFormat(pixelFormat);// 只有pixelFormat,width,height三个参数任意一个不为空才会进行像素格式转换
recorder.setImageScalingFlags(imageScalingFlags);//缩放,像素格式转换时使用,但是并不是像素格式转换的判断参数之一
recorder.setGopSize(gopSize);//gop间隔
recorder.setFrameRate(frameRate);//帧率
recorder.setVideoBitrate(videoBitrate);
recorder.setVideoQuality(videoQuality);
 //下面这三个参数任意一个会触发音频编码
recorder.setSampleFormat(sampleFormat);//音频采样格式,使用avutil中的像素格式常量,例如:avutil.AV_SAMPLE_FMT_NONE
recorder.setAudioChannels(audioChannels);
recorder.setSampleRate(sampleRate);
recorder.start();

start中其实做了很多事情:一堆初始化操作、打开网络流、查找编码器、把format字符转换成对应的videoCodecvideoFormat等等。

  • record(Frame frame):编码方式录制,把已经解码的图像像素编码成对应的编码和格式推流出去

  • record(Frame frame, int pixelFormat):多出一个pixelFormat像素格式参数就是因为它会触发像素格式转换,如果像素格式与编码器要求的像素格式不同的时候,就会进行转换像素格式,在转换的时候同时也会转换width、height、imageScalingFlags。

  • recordImage(int width, int height, int depth, int channels, int stride, int pixelFormat, Buffer ... image):其实上面两个都是调用的这个方法。Buffer[] image是从Frame frame里的image参数获取的,所以很多小伙伴问我FrameGrabber解码出来的图像像素在哪里?看,它在frame.image里。

  • recordSamplesBuffer ... samples):用来编码录制音频的,它调用了下面的方法,Buffer ... samples是从Frameframe.samples来,所以如上所述,很多同学来问我FrameGrabber解码出来的音频采样存哪里去了?看,它还在Frame里面。

  • recordSamples(int sampleRate, int audioChannels, Buffer ... samples):用来编码录制音频的,它会触发重采样,当 samples_channels(音频通道)、samples_format(音频采样格式)和samples_rate(采样率)任意参数不为空的时候就会触发重采样。

  • recordPacket(AVPacket pkt):转封装操作,用来放未经过解码的解复用视频帧。

非常需要的注意的是,当我们在录制文件的时候,一定要保证stop()方法被调用,因为stop里面包含了写出文件头的操作,如果没有调用stop就会导致录制的文件损坏,无法被识别或无法播放。

close()方法中包含stop()release()方法

4、OpenCVFrameRecorder结构分析

OpenCVFrameRecorder的代码很简单,不到120行的代码,主要是基于opencv的videoWriter封装,流程与FrameRecorder的相同:

初始化和设置参数—>start()—>循环record(Frame frame)—>close()

在start中会初始化opencv的VideoWriterVideoWriter是opencv中用来保存视频的模块,是比较简单的。

OpenCVFrameRecorder初始化参数只有三个:filename(文件名称或者文件保存地址),imageWidth(图像宽度), imageHeight(图像高度)。但是OpenCVFrameRecorder的有作用的参数只有六个,其中pixelFormatvideoCodec这两个比较特殊。

  • pixelFormat:该参数并不像ffmpeg那样表示像素格式,这里只表示原生和灰度图,其中pixelFormat=1表示原生,pixelFormat=0表示灰度图。

  • videoCodec:这个参数,在opencv中对应的是fourCCCodec,所以编码这块的设置也要按照opencv的独特方式来设置。

filename(文件名称或者文件保存地址),imageWidth(图像宽度), imageHeight(图像高度)和frameRate(帧率)这四个参数就不过多赘述了。

调用start时其实就是初始化了opencv的VideoWriter。这个比较简单,传了上面那六个参数。

在循环中把Frame放进record中就可以一直录制,内部是通过OpenCVFrameConverterFrame转换成了Mat调用VideoWriter进行录制。

不管是stop还是close,其实都是调用的release()方法, release则是调用了opencv的VideoWriterrelease(),结构很简单。

五、帧过滤器(FrameFilter)的原理与应用

FrameFilter就是过滤音频和视频帧,并对音频和视频进行处理的一个帧处理器,用滤镜来描述可能更为贴切一点(但是由于FrameFilter还可以处理音频,所以我们还是使用“过滤器”更合适些,虽然有可能引起歧义就是了),在采集到解码后的音视频源或者图像、音频后,对解码后的数据源进行加工的过程就是FrameFilter做的事情了。

FrameFilter的一般调用处理流程

初始化和设置解码后的数据—>start()—>循环start| push(Frame frame)—>Farme pull() |循环end—>结束时调用stop释放内存

结合FrameGrabber和FrameRecorder后的FrameFilter处理流程

如下图所示:

FrameFilter只有一个实现类就是FFmpegFrameFilter

FFmpegFrameFilter的初始化及参数
//视频和音频混合过滤器初始化
FFmpegFrameFilter(String videoFilters, String audioFilters, int imageWidth, int imageHeight, int audioChannels)

//视频过滤器初始化
FFmpegFrameFilter(String filters, int imageWidth, int imageHeight)

//音频过滤器初始化
FFmpegFrameFilter(String afilters, int audioChannels)

视频需要至少三个参数:videoFilter、imageWidth和imageHeight。也就是说必须保证这三个参数(视频过滤器,图像宽度,高度)都不能为空,且图像高度和宽度大小不能太离谱。

音频过滤器需要至少两个参数:afilters 和 audioChannels。音频过滤器和音频通道

start方法

start()方法会对视频过滤器和音频过滤器进行判断,如果有视频过滤器和音频过滤器则就会初始化对应过滤器上下文。

push方法

可以同时送入视频图像像素和音频采样(Frame对象可以同时包含图像像素和音频采样数据)

  • push(Frame frame)

与上面一样,只不过可以设置视频图像像素格式

  • push(Frame frame, int pixelFormat)

上面两个方法都是这个方法的语法糖,也是一样同时支持送入视频和音频, 还支持更多参数,比如视频的宽度(width)、高度(height)、图像深度(depth),图像通道(channels)、图像跨度(stride)、像素格式(pixelFormat),图像像素缓存数据

  • pushImage(int width, int height, int depth, int channels, int stride, int pixelFormat, Buffer ... image)

只送入音频采样数据,音频通道(audioChannels), 采样率(sampleRate), 音频编码格式(sampleFormat)和音频采样缓存

  • pushSamples(int audioChannels, int sampleRate, int sampleFormat, Buffer ... samples)

  • 补充:关于图像深度(depth),图像深度是图像里用来表示一个像素点的颜色由几位构成,比如24位图像深度的1920×1080的高清图像,就表示这个高清图像它所占空间是(1920x1080x24bit/8)kb也就是:1920x1080x3 kb*

  • 关于图像通道(channels),channels*8=depth,也就说图像深度除8Bit就是图像通道数,*图像跨度(stride)就是图像一行所占长度,一般大于等于图像的宽度。

pull方法

拉取经过视频和音频过滤器处理后的视频图像Frame,如果同时设置了音频和视频过滤器,会同时通过Frame返回,如果只设置了其中一个,则Frame中存放的也是只有其中一个处理后的图像像素或者音频采样。

  • pull()

只拉取经过视频过滤器处理后的图像像素

  • Frame pullImage()

只拉取经过音频过滤器处理后的音频采样

  • Frame pullSamples()

六、FrameConverter转换工具类

FrameConverter封装了常用的转换操作,比如opencv与Frame的互转、java图像与Frame的互转以及安卓平台的Bitmap图像与Frame互转操作。

FrameConverter的子类: AndroidFrameConverterJava2DFrameConverterJavaFXFrameConverterLeptonicaFrameConverterOpenCVFrameConverter

由于JavaCV的Frame完全是仿照ffmpeg的AVFrame格式设计的,所有AVFrameFrame不存在互转,它们的数据格式基本是互通的,直接赋值即可。

1、AndroidFrameConverter互转操作

专门用于安卓平台的转换操作,用于将Bitmap和Frame进行互转,以及提供了额外的yuv转bgr操作。

//Frame转换为Bitmap
Bitmap convert(Frame frame)
 //bitmap转换为frame
Frame convert(Bitmap bitmap)
//yuv4:2:0像素转换为BGR像素
/**
 *  Convert YUV 4:2:0 SP (NV21) data to BGR, as received, for example,
 *  via {@link Camera.PreviewCallback#onPreviewFrame(byte[],Camera)}
*/
public Frame convert(byte[] data, int width, int height)

2、Java2DFrameConverter互转操作

提供了Frame和java图像BufferedImage的互转操作。

//Frame转BufferedImage图像
public BufferedImage getBufferedImage(Frame frame)
BufferedImage getBufferedImage(Frame frame, double gamma)//伽马值,用来调节图像的灰度曲线,与显示设备有关
BufferedImage getBufferedImage(Frame frame, double gamma, boolean flipChannels, ColorSpace cs)

 //BufferedImage图像转Frame
Frame getFrame(BufferedImage image)
Frame getFrame(BufferedImage image, double gamma)
Frame getFrame(BufferedImage image, double gamma, boolean flipChannels)

3、JavaFXFrameConverter互转操作

提供了JavaFX的图像ImageFrame的转换操作。

//把javaFX的图像Image转换为javacv的Frame
Frame convert(Image f)

//把Frame转换为javaFX的Image图像对象
 Image convert(Frame frame) 

4、LeptonicaFrameConverter互转操作

用于Leptonica和tesserac的PIX和Frame的互转,Leptonica是图像识别库google tesserac ocr的依赖库,也即是说该工具类一般是用于tesserac的图像PIX对象与Frame互转操作。

//Frame转tesserac的PIX图像
PIX convert(Frame frame)

//tesserac的PIX图像转Frame
Frame convert(PIX pix)

5、OpenCVFrameConverter互转操作

主要用于opencv的IplImage/MatFrame的互转操作。

//IplImage与Frame互转
//把frame转换成IplImage
IplImage convertToIplImage(Frame frame)
//把IplImage转换成frame
Frame convert(IplImage img) 

//Mat与Frame互转
//frame转换成Mat
Mat convertToMat(Frame frame)
//mat转换成frame
Frame convert(Mat mat)

七、CanvasFrame图像预览工具类

CanvasFrame是用于预览Frame图像的工具类,但是这个工具类的gama值通常是有问题的,所以显示的图像可能会偏色,但是不影响最终图像的色彩。

CanvasFrame内部是使用的swing的Canvas画板操作,使用canvas画板绘制图像。

CanvasFrame canvas = new CanvasFrame("转换apng中屏幕预览");// 新建一个窗口
canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
canvas.setAlwaysOnTop(true);

//显示画面,这个操作用来显示Frame,一般Frame从各个FrameGrabber中获取或者从各个converter转换类中而来。
canvas.showImage(frame);

发表评论