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的子类: AndroidFrameConverter
、Java2DFrameConverter
、JavaFXFrameConverter
、LeptonicaFrameConverter
、OpenCVFrameConverter
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中两个重要的结构体:AVPacket和AVFrame。
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并不支持音频读取和录制等操作,只支持视频文件、视频流媒体、图像采集设备的画面抓取。
另外需要注意的是,读取非动态图片,只能读取一帧。
通过OpenCVFrameRecorder
的grab()
抓取到的图像是Frame
,其实javaCV内部通过OpenCVFrameConverter
把opencv的Mat
转换为了Frame
,也即是说,Frame
中可以直接获取Mat
或者也可以通过OpenCVFrameConverter
实现Mat
和Frame
的互转。
2、OpenCVFrameGrabber获取的Frame和Mat之间的关系
OpenCVFrameGrabber
既然内部封装的是opencv的VideoCapture
,那么获取的数据结构应该是Mat
,通过OpenCVFrameGrabber
源码也证实了这一点,那么我们如何直接获取Mat呢?
实际上在OpenCVFrameGrabber
中把获取到的Mat转换为Frame
,并且在Frame
中使用opaque字段引用了Mat
结构体。
在使用OpenCVFrameGrabber
获取Frame
的时候不需要再进行Frame
和Mat
转换,只需要使用
Mat img = (Mat)frame.opaque;
即可获得Mat结构体。
3、OpenCVFrameRecorder录制媒体文件
OpenCVFrameRecorder
主要封装了opencv的VideoWriter
模块,用来实现视频流媒体的录制操作,支持的格式同样参考:http://mp4ra.org/#/codecs 和 https://www.fourcc.org/codecs.php 这两个列表。
通过循环recordFrame
就可以录制视频流媒体,当然如果是进行图像处理操作,得到的是Mat
,就可以通过OpenCVFrameConverter
把Mat
转换成Frame
即可进行record()
录制操作。
4、OpenCVFrameConverter
进行Mat
、 IplImage
和Frame
的互转
由于我们使用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
简称为:录制器。
FrameRecorder
与FrameGrabber
类似,也是个抽象类,抽象了所有录制器的通用方法和一些共用属性。
FrameRecorder
只有两个子类实现:FFmpegFrameRecorder
和OpenCVFrameRecorder
1、两个FrameRecorder实现类的介绍
FFmpegFrameRecorder
:使用ffmpeg作为音视频编码/图像、封装、推流、录制库。除了支持音视频录制推流之外,还可以支持gif/apng动态图录制,对于音视频这块的功能ffmpeg依然还是十分强大的保障。-
OpenCVFrameRecorder
:使用opencv的videowriter录制视频,不支持像素格式转换,根据opencv的官方介绍,如果在ffmpeg可用的情况下,opencv的视频功能可能也是基于ffmpeg的,macos下基于avfunctation。
总得来说,音视频这块还是首选 ffmpeg 的录制器。但是凡事都有例外,由于 javacv 的包实在太大,开发的时候下载依赖都要半天。为了减少程序的体积大小,如果只需要进行图像处理/图像那个识别的话只使用 opencv 的情况就能解决问题,那就
opencvFrameGrabber
和OpenCVFrameRecorder
配套使用吧,毕竟可以省很多空间。同样的,如果只是做音视频流媒体,那么能使用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字符转换成对应的videoCodec
和videoFormat
等等。
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
是从Frame
的frame.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的VideoWriter
,VideoWriter
是opencv中用来保存视频的模块,是比较简单的。
OpenCVFrameRecorder
初始化参数只有三个:filename
(文件名称或者文件保存地址),imageWidth
(图像宽度), imageHeight
(图像高度)。但是OpenCVFrameRecorder
的有作用的参数只有六个,其中pixelFormat
和videoCodec
这两个比较特殊。
pixelFormat
:该参数并不像ffmpeg那样表示像素格式,这里只表示原生和灰度图,其中pixelFormat=1
表示原生,pixelFormat=0
表示灰度图。
videoCodec
:这个参数,在opencv中对应的是fourCCCodec,所以编码这块的设置也要按照opencv的独特方式来设置。
filename
(文件名称或者文件保存地址),imageWidth
(图像宽度), imageHeight
(图像高度)和frameRate
(帧率)这四个参数就不过多赘述了。
调用start时其实就是初始化了opencv的VideoWriter
。这个比较简单,传了上面那六个参数。
在循环中把Frame放进record中就可以一直录制,内部是通过OpenCVFrameConverter
把Frame
转换成了Mat
调用VideoWriter
进行录制。
不管是stop还是close,其实都是调用的release()
方法, release则是调用了opencv的VideoWriter
的release()
,结构很简单。
五、帧过滤器(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
的子类: AndroidFrameConverter
、Java2DFrameConverter
、JavaFXFrameConverter
、LeptonicaFrameConverter
、OpenCVFrameConverter
由于JavaCV的
Frame
完全是仿照ffmpeg的AVFrame格式设计的,所有AVFrame
和Frame
不存在互转,它们的数据格式基本是互通的,直接赋值即可。
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的图像Image
和Frame
的转换操作。
//把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
/Mat
和Frame
的互转操作。
//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);