TDLR:所有DJI开发人员都将从将原始H264视频流字节数组解码为与OpenCV兼容的格式中受益。
我花了很多时间寻找一种解决方案,以将DJI的FPV提要读取为OpenCV Mat对象。我可能忽略了一些基本知识,因为我对图像编码/解码不太熟悉。
将来遇到它的开发人员可能会遇到与我同样的问题。如果DJI开发人员可以直接使用opencv而不需要第3方库,那就太好了。
我愿意在必要时使用ffmpeg或JavaCV,但这对于大多数Android开发人员来说是一大障碍,因为我们将不得不使用cpp,ndk,终端进行测试等。这两个选项似乎都很耗时。 This JavaCV H264 conversion似乎不必要地复杂。我是从this relevant question找到的。
我认为问题在于我们需要同时解码长度为6的字节数组(信息数组)和具有当前帧信息的字节数组。
基本上,DJI的FPV供稿有多种格式。
VideoFeeder.VideoDataListener中的
// The callback for receiving the raw H264 video data for camera live view
mReceivedVideoDataListener = new VideoFeeder.VideoDataListener() {
@Override
public void onReceive(byte[] videoBuffer, int size) {
//Log.d("BytesReceived", Integer.toString(videoStreamFrameNumber));
if (videoStreamFrameNumber++%30 == 0){
//convert video buffer to opencv array
OpenCvAndModelAsync openCvAndModelAsync = new OpenCvAndModelAsync();
openCvAndModelAsync.execute(videoBuffer);
}
if (mCodecManager != null) {
mCodecManager.sendDataToDecoder(videoBuffer, size);
}
}
};
@Override
public void onYuvDataReceived(final ByteBuffer yuvFrame, int dataSize, final int width, final int height) {
//In this demo, we test the YUV data by saving it into JPG files.
//DJILog.d(TAG, "onYuvDataReceived " + dataSize);
if (count++ % 30 == 0 && yuvFrame != null) {
final byte[] bytes = new byte[dataSize];
yuvFrame.get(bytes);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
if (bytes.length >= width * height) {
Log.d("MatWidth", "Made it");
YuvImage yuvImage = saveYuvDataToJPEG(bytes, width, height);
Bitmap rgbYuvConvert = convertYuvImageToRgb(yuvImage, width, height);
Mat yuvMat = new Mat(height, width, CvType.CV_8UC1);
yuvMat.put(0, 0, bytes);
//OpenCv Stuff
}
}
});
}
}
编辑:对于那些想看DJI的YUV到JPEG功能的人,这里来自示例应用程序:
private YuvImage saveYuvDataToJPEG(byte[] yuvFrame, int width, int height){
byte[] y = new byte[width * height];
byte[] u = new byte[width * height / 4];
byte[] v = new byte[width * height / 4];
byte[] nu = new byte[width * height / 4]; //
byte[] nv = new byte[width * height / 4];
System.arraycopy(yuvFrame, 0, y, 0, y.length);
Log.d("MatY", y.toString());
for (int i = 0; i < u.length; i++) {
v[i] = yuvFrame[y.length + 2 * i];
u[i] = yuvFrame[y.length + 2 * i + 1];
}
int uvWidth = width / 2;
int uvHeight = height / 2;
for (int j = 0; j < uvWidth / 2; j++) {
for (int i = 0; i < uvHeight / 2; i++) {
byte uSample1 = u[i * uvWidth + j];
byte uSample2 = u[i * uvWidth + j + uvWidth / 2];
byte vSample1 = v[(i + uvHeight / 2) * uvWidth + j];
byte vSample2 = v[(i + uvHeight / 2) * uvWidth + j + uvWidth / 2];
nu[2 * (i * uvWidth + j)] = uSample1;
nu[2 * (i * uvWidth + j) + 1] = uSample1;
nu[2 * (i * uvWidth + j) + uvWidth] = uSample2;
nu[2 * (i * uvWidth + j) + 1 + uvWidth] = uSample2;
nv[2 * (i * uvWidth + j)] = vSample1;
nv[2 * (i * uvWidth + j) + 1] = vSample1;
nv[2 * (i * uvWidth + j) + uvWidth] = vSample2;
nv[2 * (i * uvWidth + j) + 1 + uvWidth] = vSample2;
}
}
//nv21test
byte[] bytes = new byte[yuvFrame.length];
System.arraycopy(y, 0, bytes, 0, y.length);
for (int i = 0; i < u.length; i++) {
bytes[y.length + (i * 2)] = nv[i];
bytes[y.length + (i * 2) + 1] = nu[i];
}
Log.d(TAG,
"onYuvDataReceived: frame index: "
+ DJIVideoStreamDecoder.getInstance().frameIndex
+ ",array length: "
+ bytes.length);
YuvImage yuver = screenShot(bytes,Environment.getExternalStorageDirectory() + "/DJI_ScreenShot", width, height);
return yuver;
}
/**
* Save the buffered data into a JPG image file
*/
private YuvImage screenShot(byte[] buf, String shotDir, int width, int height) {
File dir = new File(shotDir);
if (!dir.exists() || !dir.isDirectory()) {
dir.mkdirs();
}
YuvImage yuvImage = new YuvImage(buf,
ImageFormat.NV21,
width,
height,
null);
OutputStream outputFile = null;
final String path = dir + "/ScreenShot_" + System.currentTimeMillis() + ".jpg";
try {
outputFile = new FileOutputStream(new File(path));
} catch (FileNotFoundException e) {
Log.e(TAG, "test screenShot: new bitmap output file error: " + e);
//return;
}
if (outputFile != null) {
yuvImage.compressToJpeg(new Rect(0,
0,
width,
height), 100, outputFile);
}
try {
outputFile.close();
} catch (IOException e) {
Log.e(TAG, "test screenShot: compress yuv image error: " + e);
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
displayPath(path);
}
});
return yuvImage;
}
选项1比选项2更可取,因为YUV格式存在质量问题。选项3也可能涉及解码器。
这是DJI自己的YUV转换产生的屏幕截图。
我已经看过很多有关如何改善YUV,消除绿色和黄色以及诸如此类的东西,但是在这一点上,如果DJI无法正确完成,我不想在那儿投入资源。
关于选项1,我知道如果必须走视频解码路线,那么FFMPEG和JavaCV似乎是不错的选择。
而且,据我了解,没有FFMPEG,OpenCV无法处理读写视频文件,但是我没有尝试读取视频文件,而是试图读取H264 / MPEG4 byte []数组。以下代码似乎获得了积极的结果。
/* Async OpenCV Code */
private class OpenCvAndModelAsync extends AsyncTask<byte[], Void, double[]> {
@Override
protected double[] doInBackground(byte[]... params) {//Background Code Executing. Don't touch any UI components
//get fpv feed and convert bytes to mat array
Mat videoBufMat = new Mat(4, params[0].length, CvType.CV_8UC4);
videoBufMat.put(0,0, params[0]);
//if I add this in it says the bytes are empty.
//Mat videoBufMat = Imgcodecs.imdecode(encodeVideoBuf, Imgcodecs.IMREAD_ANYCOLOR);
//encodeVideoBuf.release();
Log.d("MatRgba", videoBufMat.toString());
for (int i = 0; i< videoBufMat.rows(); i++){
for (int j=0; j< videoBufMat.cols(); j++){
double[] rgb = videoBufMat.get(i, j);
Log.i("Matrix", "red: "+rgb[0]+" green: "+rgb[1]+" blue: "+rgb[2]+" alpha: "
+ rgb[3] + " Length: " + rgb.length + " Rows: "
+ videoBufMat.rows() + " Columns: " + videoBufMat.cols());
}
}
double[] center = openCVThingy(videoBufMat);
return center;
}
protected void onPostExecute(double[] center) {
//handle ui or another async task if necessary
}
}
行= 4,列> 30k。我得到许多看似有效的RGB值,例如,红色= 113,绿色= 75,蓝色= 90,alpha = 220等。但是,我得到了大量的0,0,0,0值。这应该可以,因为Black为0,0,0(尽管我原本以为alpha会更高),并且图像中有一个黑色物体。我也似乎没有得到任何白色值255、255、255,即使也有很多白色区域。我没有记录整个字节,因此它可能在那里,但是我还没有看到它。
但是,当我尝试从该图像计算轮廓时,几乎总会得到力矩(中心x,y)正好在图像的中心。此错误与我的滤色镜或轮廓算法无关,因为我在python中编写了一个脚本,并测试了如何通过读取静止图像并在两个Python中获得完全相同数量的轮廓,位置等在Android中正确实现了该脚本和Android。
我注意到这与videoBuffer字节大小有关(如果可以解释为什么其他所有长度都是6,则加分)
2019-05-23 21:14:29.601 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2425
2019-05-23 21:14:29.802 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2659
2019-05-23 21:14:30.004 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:30.263 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6015
2019-05-23 21:14:30.507 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:30.766 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4682
2019-05-23 21:14:31.005 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:31.234 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 2840
2019-05-23 21:14:31.433 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4482
2019-05-23 21:14:31.664 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:31.927 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4768
2019-05-23 21:14:32.174 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:32.433 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4700
2019-05-23 21:14:32.668 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:32.864 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4740
2019-05-23 21:14:33.102 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 6
2019-05-23 21:14:33.365 21431-22086/com.dji.simulatorDemo D/VideoBufferSize: 4640
我的问题:
I.这是将h264字节读取为mat的正确格式吗?
假设格式为RGBA,则意味着row = 4,column = byte []。length和CvType.CV_8UC4。我的高度和宽度正确吗?某事告诉我YUV的高度和宽度已关闭。我得到了一些有意义的结果,但是轮廓正好在中心,就像使用H264一样。
二。 OpenCV会这样处理Android中的MP4吗?如果不是,是否需要使用FFMPEG或JavaCV?
三, int大小与它有关吗?为什么int大小有时为6,而其他时候为2400至6000?我已经听说过此框架信息与下一框架信息之间的区别,但是我不了解如何在此处应用它。
我开始认为这就是问题所在。由于我需要获取6字节数组以获取有关下一帧的信息,因此我的模数30可能不正确。那么我应该将第29或31帧作为每个帧的格式字节传递吗?如何在opencv中完成,还是注定要使用复杂的ffmpeg?我将如何加入相邻的帧/字节数组?
IV。我可以使用Imcodecs修复此问题吗?我希望opencv可以本地处理该帧是该帧的颜色还是下一帧的信息。我添加了以下代码,但是得到一个空数组:
Mat videoBufMat = Imgcodecs.imdecode(new MatOfByte(params[0]), Imgcodecs.IMREAD_UNCHANGED);
这也是空的:
Mat encodeVideoBuf = new Mat(4, params[0].length, CvType.CV_8UC4);
encodeVideoBuf.put(0,0, params[0]);
Mat videoBufMat = Imgcodecs.imdecode(encodeVideoBuf, Imgcodecs.IMREAD_UNCHANGED);
V.我应该尝试将字节转换为Android jpeg然后导入吗?为什么djis yuv解码器看起来如此复杂?这让我不必尝试使用ffmpeg或Javacv并仅坚持使用Android解码器或opencv解码器就变得审慎。
VI。我应该在什么阶段调整框架的大小以加快计算速度?
编辑: DJI支持返回给我,并确认他们没有用于执行我所描述的操作的任何示例。这是我们社区让所有人都能使用的时间!
经过进一步的研究,我认为opencv将无法处理此问题,因为opencv的android sdk不具有视频文件/ URL的功能(自产的MJPEG编解码器除外)。
那么在Android中有没有一种方法可以将其转换为mjpeg或类似格式以便进行读取?在我的应用程序中,我每秒只需要1或2帧,因此也许可以将图像另存为jpeg。
但是对于实时应用程序,我们可能需要编写自己的解码器。请提供帮助,以便我们对所有人开放!这个question看起来很有希望:
最佳答案
首先H264和h264是不同的。与h264 H264 x264 X264混合使用也是ez。上次使用时,我记得我在DJI设备上使用了h264选项。确保选择正确的编解码器
ffmpeg和ffplay将直接起作用。我记得Opencv可以在这两个基础之上构建。因此使用FFMEPG / FFSHOW插件转换为cv::Mat应该不难。遵循文档
上次,我必须使用DJI PSDK。并且它们仅允许使用H.264的UDP端口udp://192.168.5.293:23003进行流传输
因此,我编写了一个简单的ffmpeg接口(interface)以流式传输到PSDK。但是我必须事先调试它。因此,我使用ffplay来显示此网络流以证明其有效。这是显示流的脚本。因此,您必须在此之上作为opencv插件工作
ffplay -f h264 -i udp://192.168.1.45:23003
关于android - 如何将DJI H264 FPV Feed读取为OpenCV Mat对象?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56284630/