【发布时间】:2019-10-10 14:00:21
【问题描述】:
TDLR:所有 DJI 开发人员都会受益于将原始 H264 视频流字节数组解码为与 OpenCV 兼容的格式。
我花了很多时间寻找将 DJI 的 FPV 提要读取为 OpenCV Mat 对象的解决方案。我可能忽略了一些基本的东西,因为我对图像编码/解码不太熟悉。
遇到它的未来开发人员可能会遇到一堆与我相同的问题。如果 DJI 开发者可以直接使用 opencv 而不需要第三方库,那就太好了。
如有必要,我愿意使用 ffmpeg 或 JavaCV,但这对于大多数 Android 开发人员来说是一个很大的障碍,因为我们将不得不使用 cpp、ndk、终端进行测试等。这似乎有点矫枉过正。这两种选择似乎都非常耗时。 This JavaCV H264 conversion 似乎不必要地复杂。我是从this relevant question 找到的。
我认为问题在于我们需要同时解码长度为 6 的字节数组(信息数组)和具有当前帧信息的字节数组。
基本上,DJI 的 FPV 提要有多种格式。
- VideoFeeder.VideoDataListener 中的原始 H264 (MPEG4)
// 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);
}
}
};
- DJI 也有自己的 Android 解码器示例,带有 FFMPEG 可转换为 YUV 格式。
@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
}
}
});
}
}
编辑:想看大疆的 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;
}
- DJI 似乎也有一个“getRgbaData”功能,但实际上没有一个在线示例或 DJI 提供的示例。继续谷歌“DJI getRgbaData”......只有对解释自我解释参数和返回值的api文档的引用,但没有别的。我不知道在哪里调用它,并且似乎没有像 YUV 那样的回调函数。不能直接从 h264b 字节数组中调用,但或许可以从 yuv 数据中获取。
选项 1 比选项 2 更可取,因为 YUV 格式存在质量问题。选项 3 也可能涉及解码器。
我看过很多关于如何改进 YUV、去除绿色和黄色等等的东西,但是在这一点上,如果 DJI 做不好,我不想在那里投入资源。
关于选项 1,我知道如果我必须走视频解码路线,FFMPEG 和 JavaCV 似乎是不错的选择。
此外,据我了解,OpenCV 无法在没有 FFMPEG 的情况下读取和写入视频文件,但我不是在尝试读取视频文件,而是在尝试读取 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 值,例如 red = 113, green=75, blue=90, alpha=220 作为一个虚构的例子;但是,我得到了大量的 0,0,0,0 值。这应该没问题,因为黑色是 0,0,0(尽管我认为 alpha 会更高)并且我的图像中有一个黑色物体。我似乎也没有得到任何白色值 255、255、255,即使也有很多白色区域。我没有记录整个字节,所以它可能在那里,但我还没有看到它。
但是,当我尝试从该图像计算轮廓时,我几乎总是得到矩(中心 x,y)正好位于图像的中心。此错误与我的颜色过滤器或轮廓算法无关,因为我在 python 中编写了一个脚本并测试我通过读取静止图像并在两个 Python 中获得完全相同数量的轮廓、位置等在 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
我的问题:
我。这是将 h264 字节读取为 mat 的正确格式吗? 假设格式是 RGBA,这意味着 row = 4 和 columns = byte[].length 和 CvType.CV_8UC4。我的高度和宽度正确吗?有些东西告诉我 YUV 高度和宽度已关闭。我得到了一些有意义的结果,但轮廓正好在中心,就像 H264 一样。
二。 OpenCV 是否像这样在 android 中处理 MP4?如果没有,我们是否需要使用 FFMPEG 或 JavaCV?
三。 int大小是否与它有关?为什么 int 大小有时是 6,有时是 2400 到 6000?我听说过此帧信息和有关下一帧的信息之间的区别,但我根本没有足够的知识知道如何在这里应用它。
我开始认为这就是问题所在。由于我需要获取 6 字节数组以获取有关下一帧的信息,因此我的模 30 可能不正确。那么我应该将第 29 帧或第 31 帧作为每个帧的格式字节传递吗?这是如何在 opencv 中完成的,还是我们注定要使用复杂的 ffmpeg?我将如何加入相邻的帧/字节数组?
四。我可以使用 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 解码器。
六。我应该在什么阶段调整帧大小以加快计算速度?
编辑: DJI 支持人员回复了我,并确认他们没有任何样品可用于我所描述的操作。现在是我们社区向所有人提供此功能的时候了!
经过进一步研究,我认为 opencv 无法处理此问题,因为 opencv 的 android sdk 没有视频文件/url 的功能(除了自制的 MJPEG 编解码器)。
那么,Android 中有没有一种方法可以转换为 mjpeg 或类似格式以便阅读?在我的应用程序中,我每秒只需要 1 或 2 帧,所以也许我可以将图像保存为 jpeg。
但对于实时应用程序,我们可能需要编写自己的解码器。请提供帮助,以便我们可以将其提供给所有人!这个question 看起来很有希望:
【问题讨论】:
-
您是否尝试使用 imshow 可视化 yuv 框架?在这种情况下,Y 通道将映射到蓝色,U 将映射到绿色,V 到红色通道以进行可视化。
-
正如我所提到的,这是 dji 原生 yuv 到 jpeg 的转换。这就是保存到我的平板电脑上的 jpeg。我没有使用 imshow
-
这绝对是一个很好的建议,说明它为什么不起作用。但我只是使用 djis 示例代码来产生这些结果
-
我在 DJI 的示例代码中添加了 YUV 保存到 JPEG 功能,供有兴趣的朋友参考
标签: android opencv ffmpeg javacv dji-sdk