Android相机开发(六): 高效实时处理预览帧数据
Android Camera Develop: process preview frames in real time efficiently
概述
本篇我们暂时不介绍像相机APP增加新功能,而是介绍如何处理相机预览帧数据。想必大多数人都对处理预览帧没有需求,因为相机只需要拿来拍照和录像就好了,实际上本篇和一般的相机开发也没有太大联系,但因为仍然是在操作Camera类,所以还是归为相机开发。处理预览帧简单来说就是对相机预览时的每一帧的数据进行处理,一般来说如果相机的采样速率是30fps的话,一秒钟就会有30个帧数据需要处理。帧数据具体是什么?如果你就是奔着处理帧数据来的话,想必你早已知道答案,其实就是一个byte类型的数组,包含的是YUV格式的帧数据。本篇仅介绍几种高效地处理预览帧数据的方法,而不介绍具体的用处,因为拿来进行人脸识别、图像美化等又是长篇大论了。
本篇在Android相机开发(二): 给相机加上偏好设置的基础上介绍。预览帧数据的处理通常会包含大量的计算,从而导致因为帧数据太多而处理效率低下,以及衍生出的预览画面卡顿等问题。本篇主要介绍分离线程优化画面显示,以及通过利用HandlerThread、Queue、ThreadPool和AsyncTask来提升帧数据处理效率的方法。
准备
为了简单起见,我们在相机开始预览的时候就开始获取预览帧并进行处理,为了能更清晰地分析这个过程,我们在UI中“设置”按钮之下增加“开始”和“停止”按钮以控制相机预览的开始与停止。
修改UI
修改activity_main.xml,将
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置" />
替换为
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:orientation="vertical">
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置" />
<Button
android:id="@+id/button_start_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始" />
<Button
android:id="@+id/button_stop_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止" />
</LinearLayout>
这样增加了“开始”和“停止”两个按钮。
绑定事件
修改mainActivity,将原onCreate()
中初始化相机预览的代码转移到新建的方法startPreview()
中
public void startPreview() {
final CameraPreview mPreview = new CameraPreview(this);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
SettingsFragment.passCamera(mPreview.getCameraInstance());
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SettingsFragment.setDefault(PreferenceManager.getDefaultSharedPreferences(this));
SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));
Button buttonSettings = (Button) findViewById(R.id.button_settings);
buttonSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getFragmentManager().beginTransaction().replace(R.id.camera_preview, new SettingsFragment()).addToBackStack(null).commit();
}
});
}
同时再增加一个stopPreview()
方法,用来停止相机预览
public void stopPreview() {
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.removeAllViews();
}
stopPreview()
获取相机预览所在的FrameLayout
,然后通过removeAllViews()
将相机预览移除,此时会触发CameraPreview
类中的相关结束方法,关闭相机预览。
现在onCreate()
的工作就很简单了,只需要将两个按钮绑定上对应的方法就好了
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button buttonStartPreview = (Button) findViewById(R.id.button_start_preview);
buttonStartPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startPreview();
}
});
Button buttonStopPreview = (Button) findViewById(R.id.button_stop_preview);
buttonStopPreview.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopPreview();
}
});
}
运行试试
现在运行APP不会立即开始相机预览了,点击“开始”按钮屏幕上才会出现相机预览画面,点击“停止”则画面消失,预览停止。
基本的帧数据获取和处理
这里我们首先实现最基础,也是最常用的帧数据获取和处理的方法;然后看看改进提升性能的方法。
基础
获取帧数据的接口是Camera.PreviewCallback
,实现此接口下的onPreviewFrame(byte[] data, Camera camera)
方法即可获取到每个帧数据data
。所以现在要做的就是给CameraPreview
类增加Camera.PreviewCallback
接口声明,再在CameraPreview
中实现onPreviewFrame()
方法,最后给Camera
绑定此接口。这样相机预览时每产生一个预览帧,就会调用onPreviewFrame()
方法,处理预览帧数据data
。
在CameraPreview中,修改
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback
为
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback
加入Camera.PreviewCallback
接口声明。
加入onPreviewFrame()
的实现
public void onPreviewFrame(byte[] data, Camera camera) {
Log.i(TAG, "processing frame");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这里并没有处理帧数据data
,而是暂停0.5秒模拟处理帧数据。
在surfaceCreated()
中getCameraInstance()
这句的下面加入
mCamera.setPreviewCallback(this);
将此接口绑定到mCamera
,使得每当有预览帧生成,就会调用onPreviewFrame()
。
运行试试
现在运行APP,点击“开始”,一般在屏幕上观察不到明显区别,但这里其实有两个潜在的问题。其一,如果你这时点击“设置”,会发现设置界面并不是马上出现,而是会延迟几秒出现;而再点击返回键,设置界面也会过几秒才消失。其二,在logcat中可以看到输出的"processing frame"
,大约0.5秒输出一条,因为线程睡眠设置的是0.5秒,所以一秒钟的30个帧数据只处理了2帧,剩下的28帧都被丢弃了(这里没有非常直观的方法显示剩下的28帧被丢弃了,但事实就是这样,不严格的来说,当新的帧数据到达时,如果onPreviewFrame()
正在执行还没有返回,这个帧数据就会被丢弃)。
与UI线程分离
问题分析
现在我们来解决第一个问题。第一个问题的原因很简单,也是Android开发中经常碰到的:UI线程被占用,导致UI操作卡顿。在这里就是onPreviewFrame()
会阻塞线程,而阻塞的线程就是UI线程。
onPreviewFrame()
在哪个线程执行?官方文档里有相关描述:
Called as preview frames are displayed. This callback is invoked on the event thread open(int) was called from.
意思就是onPreviewFrame()
在执行Camera.open()
时所在的线程运行。而目前Camera.open()
就是在UI线程中执行的(因为没有创建新进程),对应的解决方法也很简单了:让Camera.open()
在非UI线程执行。
解决方法
这里使用HandlerThread来实现。HandlerThread会创建一个新的线程,并且有自己的loop,这样通过Handler.post()
就可以确保在这个新的线程执行指定的语句。虽然说起来容易,但还是有些细节问题要处理。
先从HandlerThread下手,在CameraPreview中加入
private class CameraHandlerThread extends HandlerThread {
Handler mHandler;
public CameraHandlerThread(String name) {
super(name);
start();
mHandler = new Handler(getLooper());
}
synchronized void notifyCameraOpened() {
notify();
}
void openCamera() {
mHandler.post(new Runnable() {
@Override
public void run() {
openCameraOriginal();
notifyCameraOpened();
}
});
try {
wait();
} catch (InterruptedException e) {
Log.w(TAG, "wait was interrupted");
}
}
}
CameraHandlerThread
继承自HandlerThread,在构造函数中就tart()
启动这个Thread,并创建一个handler。openCamera()
要达到的效果是在此线程中执行mCamera = Camera.open();
,因此通过handler.post()
在Runnable()
中执行,我们将要执行的语句封装在openCameraOriginal()
中。使用notify-wait是为安全起见,因为post()
执行会立即返回,而Runnable()
会异步执行,可能在执行post()
后立即使用mCamera
时仍为null
;因此在这里加上notify-wait控制,确认打开相机后,openCamera()
才返回。
接下来是openCameraOriginal()
,在CameraPreview中加入
private void openCameraOriginal() {
try {
mCamera = Camera.open();
} catch (Exception e) {
Log.d(TAG, "camera is not available");
}
}
这个不用解释,就是封装成了方法。
最后将getCameraInstance()
修改为
public Camera getCameraInstance() {
if (mCamera == null) {
CameraHandlerThread mThread = new CameraHandlerThread("camera thread");
synchronized (mThread) {
mThread.openCamera();
}
}
return mCamera;
}
这个也很容易理解,就是交给CameraHandlerThread
来处理。
运行试试
现在运行APP,会发现第一个问题已经解决了。
处理帧数据
接下来解决第二个问题,如何确保不会有帧数据被丢弃,即保证每个帧数据都被处理。解决方法的中心思想很明确:让onPreviewFrame()
尽可能快地返回,不至于丢弃帧数据。
下面介绍4种比较常用的处理方法:HandlerThread、Queue、AsyncTask和ThreadPool,针对每一种方法简单分析其优缺点。
HandlerThread
简介
采用HandlerThread就是利用Android的Message Queue来异步处理帧数据。流程简单来说就是onPreviewFrame()
调用时将帧数据封装为Message,发送给HandlerThread,HandlerThread在新的线程获取Message,对帧数据进行处理。因为发送Message所需时间很短,所以不会造成帧数据丢失。
实现
新建ProcessWithHandlerThread
类,内容为
public class ProcessWithHandlerThread extends HandlerThread implements Handler.Callback {
private static final String TAG = "HandlerThread";
public static final int WHAT_PROCESS_FRAME = 1;
public ProcessWithHandlerThread(String name) {
super(name);
start();
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case WHAT_PROCESS_FRAME:
byte[] frameData = (byte[]) msg.obj;
processFrame(frameData);
return true;
default:
return false;
}
}
private void processFrame(byte[] frameData) {
Log.i(TAG, "test");
}
}
ProcessWithHandlerThread
继承HandlerThread
和Handler.Callback
接口,此接口实现handleMessage()
方法,用来处理获得的Message。帧数据被封装到Message的obj
属性中,用what
进行标记。processFrame()
即处理帧数据,这里仅作示例。
下面要在CameraPreview
中实例化ProcessWithHandlerThread
,绑定接口,封装帧数据,以及发送Message。
在CameraPreview
中添加新的成员变量
private static final int PROCESS_WITH_HANDLER_THREAD = 1;
private int processType = PROCESS_WITH_HANDLER_THREAD;
private ProcessWithHandlerThread processFrameHandlerThread;
private Handler processFrameHandler;
在构造函数末尾增加
switch (processType) {
case PROCESS_WITH_HANDLER_THREAD:
processFrameHandlerThread = new ProcessWithHandlerThread("process frame");
processFrameHandler = new Handler(processFrameHandlerThread.getLooper(), processFrameHandlerThread);
break;
}
注意这里的new Handler()
同时也在绑定接口,让ProcessWithHandlerThread
处理接收到的Message。
修改onPreviewFrame()
为
public void onPreviewFrame(byte[] data, Camera camera) {
switch (processType) {
case PROCESS_WITH_HANDLER_THREAD:
processFrameHandler.obtainMessage(ProcessWithHandlerThread.WHAT_PROCESS_FRAME, data).sendToTarget();
break;
}
}
这里将帧数据data
封装为Message,并发送出去。
运行试试
现在运行APP,在logcat中会出现大量的"test",你也可以自己修改processFrame()
进行测试。
分析
这种方法就是灵活套用了Android的Handler机制,借助其消息队列模型Message Queue解决问题。存在的问题就是帧数据都封装为Message一股脑丢给Message Queue会不会超出限度,不过目前还没遇到。另一问题就是Handler机制可能过于庞大,相对于拿来处理这个问题不太“轻量级”。
Queue
简介
Queue方法就是利用Queue建立帧数据队列,onPreviewFrame()
负责向队尾添加帧数据,而由处理方法在队头取出帧数据并进行处理,Queue就是缓冲和提供接口的角色。
实现
新建ProcessWithQueue
类,内容为
public class ProcessWithQueue extends Thread {
private static final String TAG = "Queue";
private LinkedBlockingQueue<byte[]> mQueue;
public ProcessWithQueue(LinkedBlockingQueue<byte[]> frameQueue) {
mQueue = frameQueue;
start();
}
@Override
public void run() {
while (true) {
byte[] frameData = null;
try {
frameData = mQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
processFrame(frameData);
}
}
private void processFrame(byte[] frameData) {
Log.i(TAG, "test");
}
}
ProcessWithQueue
实例化时由外部提供Queue。为能够独立处理帧数据以及随时处理帧数据,ProcessWithQueue
继承Thread
,并重载了run()
方法。run()
方法中的死循环用来随时处理Queue中的帧数据,mQueue.take()
在队列空时阻塞,因此不会造成循环导致的CPU占用。processFrame()
即处理帧数据,这里仅作示例。
下面要在CameraPreview
中创建队列并实例化ProcessWithQueue
,将帧数据加入到队列中。
在CameraPreview
中添加新的成员变量
private static final int PROCESS_WITH_QUEUE = 2;
private ProcessWithQueue processFrameQueue;
private LinkedBlockingQueue<byte[]> frameQueue;
将
private int processType = PROCESS_WITH_THREAD_POOL;
修改为
private int processType = PROCESS_WITH_QUEUE;
在构造函数的switch中加入
case PROCESS_WITH_QUEUE:
frameQueue = new LinkedBlockingQueue<>();
processFrameQueue = new ProcessWithQueue(frameQueue);
break;
这里使用LinkedBlockingQueue
满足并发性要求,由于只操作队头和队尾,采用链表结构。
在onPreviewFrame()
的switch中加入
case PROCESS_WITH_QUEUE:
try {
frameQueue.put(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
将帧数据加入到队尾。
运行试试
现在运行APP,在logcat中会出现大量的"test",你也可以自己修改processFrame()
进行测试。
分析
这种方法可以简单理解为对之前的HandlerThread方法的简化,仅用LinkedBlockingQueue
来实现缓冲,并且自己写出队列处理方法。这种方法同样也没有避开之前说的缺点,如果队列中的帧数据不能及时处理,就会造成队列过长,占用大量内存。但优点就是实现简单方便。
AsyncTask
简介
AsyncTask方法就是用到了Android的AsyncTask类,这里就不详细介绍了。简单来说每次调用AsyncTask都会创建一个异步处理事件来异步执行指定的方法,在这里就是将普通的帧数据处理方法交给AsyncTask去执行。
实现
新建ProcessWithAsyncTask
类,内容为
public class ProcessWithAsyncTask extends AsyncTask<byte[], Void, String> {
private static final String TAG = "AsyncTask";
@Override
protected String doInBackground(byte[]... params) {
processFrame(params[0]);
return "test";
}
private void processFrame(byte[] frameData) {
Log.i(TAG, "test");
}
}
ProcessWithAsyncTask
继承AsyncTask
,重载doInBackground()
方法,输入为byte[]
,返回String
。doInBackground()
内的代码就是在异步执行,这里就是processFrame()
,处理帧数据,这里仅作示例。
下面要在CameraPreview
中实例化ProcessWithAsyncTask
,将帧数据交给AsyncTask。与之前介绍的方法不一样,每次处理新的帧数据都要实例化一个新的ProcessWithAsyncTask
并执行。
在CameraPreview
中添加新的成员变量
private static final int PROCESS_WITH_ASYNC_TASK = 3;
将
private int processType = PROCESS_WITH_QUEUE;
修改为
private int processType = PROCESS_WITH_ASYNC_TASK;
在onPreviewFrame()
的switch中加入
case PROCESS_WITH_ASYNC_TASK:
new ProcessWithAsyncTask().execute(data);
break;
实例化一个新的ProcessWithAsyncTask
,向其传递帧数据data
并执行。
运行试试
现在运行APP,在logcat中会出现大量的"test",你也可以自己修改processFrame()
进行测试。
分析
这种方法代码简单,但理解其底层实现有难度。AsyncTask实际是利用到了线程池技术,可以实现异步和并发。其相对之前的方法的优点就在于并发性高,但也不能无穷并发下去,还是会受到帧处理时间的制约。另外根据官方文档中的介绍,AsyncTask的出现主要是为解决UI线程通信的问题,所以在这里算旁门左道了。AsyncTask相比前面的方法少了“主控”的部分,可能满足不了某些要求。
ThreadPool
简介
ThreadPool方法主要用到的是Java的ThreadPoolExecutor类,想必之前的AsyncTask就显得更底层一些。通过手动建立线程池,来实现帧数据的并发处理。
实现
新建ProcessWithThreadPool
类,内容为
public class ProcessWithThreadPool {
private static final String TAG = "ThreadPool";
private static final int KEEP_ALIVE_TIME = 10;
private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
private BlockingQueue<Runnable> workQueue;
private ThreadPoolExecutor mThreadPool;
public ProcessWithThreadPool() {
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = corePoolSize * 2;
workQueue = new LinkedBlockingQueue<>();
mThreadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, KEEP_ALIVE_TIME, TIME_UNIT, workQueue);
}
public synchronized void post(final byte[] frameData) {
mThreadPool.execute(new Runnable() {
@Override
public void run() {
processFrame(frameData);
}
});
}
private void processFrame(byte[] frameData) {
Log.i(TAG, "test");
}
}
ProcessWithThreadPool
构造函数建立线程池,corePoolSize
为并发度,这里就是处理器核心个数,线程池大小maximumPoolSize
则被设置为并发度的两倍。post()
则用来通过线程池执行帧数据处理方法。processFrame()
即处理帧数据,这里仅作示例。
下面要在CameraPreview
中实例化ProcessWithThreadPool
,将帧数据交给ThreadPool。
在CameraPreview
中添加新的成员变量
private static final int PROCESS_WITH_THREAD_POOL = 4;
private ProcessWithThreadPool processFrameThreadPool;
将
private int processType = PROCESS_WITH_ASYNC_TASK;
修改为
private int processType = PROCESS_WITH_THREAD_POOL;
在构造函数的switch中加入
case PROCESS_WITH_THREAD_POOL:
processFrameThreadPool = new ProcessWithThreadPool();
break;
在onPreviewFrame()
的switch中加入
case PROCESS_WITH_THREAD_POOL:
processFrameThreadPool.post(data);
break;
将帧数据交给ThreadPool。
运行试试
现在运行APP,在logcat中会出现大量的"test",你也可以自己修改processFrame()
进行测试。
分析
ThreadPool方法相比AsyncTask代码更清晰,显得不太“玄乎”,但两者的思想是一致的。ThreadPool方法在建立线程池时有了更多定制化的空间,但同样没能避免AsyncTask方法的缺点。
一点唠叨
上面介绍的诸多方法都只是大概描述了处理的思想,在实际使用时还要根据需求去修改,但大体是这样的流程。因为实时处理缺乏完善的测试方法,所以bug也会经常存在,还需要非常小心地去排查;比如处理的帧中丢失了两三帧就很难发现,即使发现了也不太容易找出出错的方法,还需要大量的测试。
上面介绍的这些方法都是根据我踩的无数坑总结出来的,因为一直没找到高质量的介绍实时预览帧处理的文章,所以把自己知道的一些知识贡献出来,能够帮到有需要的人就算达到目的了。
关于帧数据和YUV格式等的实际处理问题,可以参考我之前写的一些Android视频解码和YUV格式解析的文章,也希望能够帮到你。
DEMO
本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-ProcessFrames。
参考
- Camera | Android Developers
- Camera.PreviewCallback | Android Developers
- android - Best use of HandlerThread over other similar classes - Stack Overflow
- Using concurrency to improve speed and performance in Android – Medium
- HandlerThread | Android Developers
- LinkedBlockingQueue | Android Developers
- AsyncTask | Android Developers
- ThreadPoolExecutor (Java Platform SE 7 )
博主,我自己弄了个queue,每次previewFrame时就加一个runnable进去。然后开一个Thread一直取queue,然后执行。1S不到就OOM了。该怎么办
非常感谢您的分享,我也学到了;但是还有个问题,怎么释放这个Camera呢?
Android推荐的释放方法是在Activity.onPause()里调用Camera的release()方法~
楼主能具体点吗?想要保存图片和视频。能有代码更好。感谢楼主
博主给了链接呀
非常感谢,这几天一直在找这方面的教程,讲解的很细致,目前需要预览帧数据,对其编解码实现视频实时通话,满足带宽要求,需要细细学习下了。
目前搜集到的程序,看到处理帧数据的有第一种和第二种,另外两个没看到。
这个能转载吗?想转载简书上,收藏
请君自取^_^,记得注明出处就好啦~
博主,您好,我现在用的金山直播sdk,做的视频直播,我现在需要在推流之前对每一帧进行处理,但是sdk对于camera进行了很好的封装,博主提到的这个camera的绑定就不能用了,mCamera.setPreviewCallback(this); 我现在找不到获取预览帧的入口
焦急等待博主的回复。。。
这个...你只能去看SDK文档或者看源码,找到从哪可以拿到这个原始帧哟~实在不行的话,试试在SDK发送帧数据的时候绑个callback?
博主您好,我在sdk中找到了获取每一帧的回调,在回调中对视频数据做了相应的处理,但是推流到服务端的视频仍是未处理前的,这个是什么原因,跪求博主回复,,,
要是你能确定你是对回调的数据做当场处理(没有复制整个数组),而实际SDK发送的数据是没作处理的,那唯一的可能就是SDK中某个地方对数组作了一次复制操作,导致你处理的数组并没有后续继续由SDK处理。
比如SDK如果自己做了YUV向RGB转换,那么很自然转换完后原始的YUV数组就不再使用而只使用新创建的RGB数组了,而你要是对YUV数据进行处理就影响不到SDK的输出了。
我觉得博主说的很对,我就是对预览返回的yuv帧数据进行了处理,应该是sdk内部有覆盖操作,所以我做的这些操作就传不上去了☹️☹️
我想问一下,拿到的帧数据过多,能给加上时间限制比如0.5秒时间间隔获取一张数据吗
可以的哟,你也以在onPreviewFrame()里加上时间间隔判断,这样就能略过不需要的帧,但时间可能不会相当精确哟
看了博主的一些列文章,收货很大,也给博主一个建议,建议使用mCamera.setPreviewCallbackWithBuffer(this)代替mCamera.setPreviewCallback(this),否则内存抖动太厉害了,影响性能
感谢反馈,等有空就试试~性能这块确实是个问题
博主,你好。我按照你的代码,在onPreviewFrame()中添加了对预览帧进行镜像的操作,就是直接将onPreviewFrame()函数的data,以及相机尺寸输入到函数mirror中,但是在手机上没有实现效果,博主知道是什么原因吗。希望博主能回答一下,谢谢~
抱歉这几天太忙没看到~
不知道你说的mirror是什么哎,如果是需要对显示的内容进行修改的话,是需要调用另外的API来重新渲染的哟~
谢谢博主回复~以前从来没有接触过安卓开发,然后导师就让我做安卓上面的人脸识别。。我按照博主的方法,然后想试试看对预览帧进行镜像看看效果,那个mirror函数是对预览帧的数据进行左右镜像。想请问一下博主要对显示内容做修改的话用哪方面的api啊。。
抱歉唉不知道为啥没收到你的评论通知~
这块我还没折腾过所以不太了解,听说是用canvas之类的,你可以去找找人脸识别相关的代码看看被人是怎么把数据重新显示在屏幕上的~
全网最实在的相机开发从入门到精通系列博客!每一个初学者可能不懂的地方都做了说明讲解,而不是其他博文那种需要一定开发经验的。受益良多
在threadPool 方案中,public class ProcessWithThreadPool {
private static final String TAG = "ThreadPool"; private static final int KEEP_ALIVE_TIME = 10; private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;可以这样申明 TimeUnit成员吗?
ProcessWithThreadPool 本身不是静态的?
抱歉不太明白你说的意思,ProcessWithThreadPool是在实例化之后使用的,private static final是普遍的class级常量声明方式
protected String doInBackground(byte[]... params) {
processFrame(params[0]); return "test"; }你好,想请问下为什么在这块处理的是params[0]?而不是params
这个是Java的语法,byte[]... params代表可以接受非特定数目的byte[]变量,全部打包为params,所以params的类型实际是byte[][]。
那params是视频流的意思?意思就是有很多帧数据
这个只是Android AsyncTask的语法规定而已,就是看起来有些怪异,在这里实际上只有params[0]用到;多帧实际上是多次调用这个方法。
博主你好,我现在想要做一个手机上的实时tracking实现,不知道这个预览帧可不可以作为视频流进行处理?对这方面我不是太了解,希望您能给稍微解个惑。
你说的“作为视频流处理”是指把预览帧编码成视频么?这个是可行的,简单来说就是把拿到的预览帧和MediaCodec对接,给MediaCodec喂YUV的byte[]数据,最后从MediaCodec拿到如H.264的mp4文件。
谢谢博主的回答,不需要编码成视频,我需要预览帧喂给我的c++tracking(jni)代码,然后把实时的tracking画面显示出来
不知道我说清楚了没?。。。有点然
抱歉回复晚了。
这个我以前试过是可以的,把JNI调用当成普通方法用就好了。关键点在预览帧是YUV格式,JNI处理后也是YUV格式,想要把处理后的数据显示在屏幕上的话就得转成RGB,这个地方效率比较低很头疼。
嗯嗯,用sdl也比较麻烦。。麻烦一个接着一个,模型的初始化只能一次,单纯要每次处理一帧,对于tracking来说,涉及到模型的初始化和训练的问题,JNI作为方法的话,每次调用jni都重新初始化。。。所以现在正在想办法解决这个问题
博主,如果使用了MediaRecorder进行录像,此时在很多机型上将无法触发onPreviewFrame,有什么好的解决办法么
有一个解决办法是用onPreviewFrame()结合MediaCodec来模拟MediaRecorder,具体就是从onPreviewFrame()拿到每一帧的数据(这时候你可以干自己想干的事情),然后把帧数据喂给MediaCodec来硬编码,这样从效率上来说是可行的,但我不太确定视频的质量与MediaRecorder是否有区别。
我现在改成用TextureView来做相机预览了,然后开线程自己去回调TextureView.getBitmap的结果,就不用onPreviewFrame了,感觉这种办法不是很好,但是也能先凑合...MediaCodec之前也试了,但是问题很多,自己不太会用,博主如果有好的例子的话希望分享一下:)
onPreviewFrame 感觉获取到数据 有延迟额,断点调试感觉会延迟一帧的样子,不是各位有没遇到这种情况!
getBitmap会导致预览卡顿,有遇到过吗?287690562 qq ,可以一起交流,我们现在也是做录像的同时需要跟踪和抓拍功能
getbitmap可以设定图的大小的 我设置的比较小 就不会卡了 我主要是做物体跟随 也不需要特别高的分辨率
抱歉回复晚了,getBitamp()可以用但是肯定不能达到30fps,你要是对帧速率没有要求的话这个方法倒是可以用的。
例子的话网上应该有一些示例代码,但MediaCodec确实难以调试~
博主有没有考虑写一篇基于camera2 API的实时处理预览帧的文章呢?最近要测试一个c++人脸检测库在手机上的表现,希望fps不会限制到库的表现。
目前有计划写一份基于Camera2的相机开发教程,但限于时间和精力,短期内可能指望不上咯~你可以看看一些基于Camera2的相机开发DEMO(比如Google自己写的),套用上这里的处理部分就好了。
非常感谢博主!要是早点儿看到这篇博客就能省去我大量的时间了!我也需要处理预览帧中的每一帧数据,但一开始就是将所有处理结果都写在了onPreviewFrame()中,后来参考了一些人脸检测Demo中的做法,用arraycopy()方法先将数据传出再处理,掉帧的情况少了很多,现在准备试试博主的方法。
最后要咨询博主一个问题~我目前对获取到的预览帧中的某一块区域感兴趣,希望截取下来单独进行处理,但每两帧之间感兴趣区域的位置和大小可能都不固定(每一帧的位置可以确定)。谷歌、百度都不知道怎么搜索,跪求博主大大提供一些方法或者思路!
抱歉这几天太忙了~
我写过定位和识别二维码的相关代码,说说我的理解。“截取”的实现一般就是定位的过程了,如果你一定要“截取”帧中的一部分保存下来的话,这个是有一定难度的,我现在想到的方法是定位到后把对应区域的矩形NV21数据抠出来保存,等跑完了再开始处理抠出来的数据(因为33ms不太可能把NV21压成jpg)。
如果你只是需要处理的话,还是尽量优化代码让整个处理过程控制在33ms内咯
非常感谢博主的回答!我现在也在考虑先将数据得到,之后再处理,目前正在获取NV21中的Y数据分量。感觉自己给自己挖的坑太深了。。哎。。
另外只用作调试的话,你不如先用相机拍视频拿到视频文件,然后爱怎么折腾怎么折腾了,说不定还能直接解成RGB帧了开始愉快玩耍~
嗯,前期就是用视频文件处理,一些东西已经做好了,想实时处理,谁知道碰到这么多事儿。博主,还有一个疑问,onPreviewFrame的回调data[]的取值范围是-128~127,但是NV21的Y分量规定是0~255,我现在提取了Y分量,但是是负值,请问该如何处理?或者说有没有更好的处理方式?真是麻烦你啦~
哈哈哈~Java基础不过关。因为Java的byte没有无符号类型,直接data[i]&0xff转int就行了
https://stackoverflow.com/questions/5272388/extract-black-and-white-image-from-android-cameras-nv21-format/12702836#12702836
这个问题搞定~
博主你好,我这里现在需要拍摄1:1的视频,但是好多手机不支持1:1拍摄,所以我想拿到预览帧数据后,裁剪这些数据,请问该怎么做呢?
预览帧数据是NV21格式的数组,你需要做的是先得到预览帧的分辨率,这样知道数组的内容分布,然后理解NV21格式,就可以试着新建一个数组然后把需要的内容抠出来拼成1:1分辨率的NV21格式数组然后继续处理啦。
不过你说要拍成视频,我就很怀疑这么做能不能满足效率要求了,你可以先试试看处理每一帧的耗时。
先录制,录制完后一并使用ffmpeg剪裁处理可以么?
当然可以啦,你可以看看ffmpeg有没有提供相关的API,不需要实时的话思路就开阔多了
默认的AsyncTask不是只有一个队列在顺序执行吗
抱歉这些天太忙没看到。
确实(很久以前)改造后的AsyncTask是串行执行的,所以我说是“旁门左道”,但是确实是在新的线程里处理呢。
博主您好,看了您的文章收货很大,之前用的相机预览程序很耗内存,改成博主的效果好多了。不过我实际使用的时候有个问题,就是我需要对相机帧进行处理,希望把帧数据转换成RGB的矩阵格式。在我之前用的程序中相机帧数据格式为CvCameraViewFrame,可以直接转换成矩阵形式。不知道博主Byte格式的帧数据能不能也转换成包含RGB像素值的矩阵格式??谢谢博主!
其实就是NV21转RGB了,这个网上有很多相关的示例,包括Android上的OpenCV也有API提供支持。简单点就是当场算,缺点是慢;复杂点就是OpenGL来加速,缺点就是OpenGL写起来有点复杂;最后就是用OpenCV这类的框架了,缺点是太重量级。
你好楼主,有个问题想请教一下,如何在调用安卓自身视频录制方法的同时获取帧数据进行实时分析,查阅了很多文献,好像两个方法是不能同时运行的,不知道你是否有办法解决。现在我唯一的思路是不调用安卓本身的视频录制,自己手动对帧数据进行处理并转成视频,但这种方法的缺陷在于:
如果对每一帧实时处理,会导致掉帧。如果将从onPreviewFrame方法返回的每一帧NV21数据存在内存中,耗费太大。望不吝赐教!
最近有点忙,没时间尝试能不能录制时同时获取帧,你可以试试在绑定onPreviewFrame的情况下(就是实时获取每一帧),同时调用录像的API,看看onPreviewFrame还能不能拿到数据,不能的话只能说目前是不行的。
把收到的每一帧拼成视频是可行的,记得用MediaCodec拼,效率很高的。
掉帧是因为onPreviewFrame()方法耗时过长(函数内容过多),或者onPreviewFrame线程太多任务。照我那种onPreviewFrame拿到数据马上放队列的方法是不会出现掉帧的(我目前就是用的这个方法,而且我需要处理每一帧,可以保证一秒钟是有正好30帧的)。这个内存消耗在我这里就是队列的消耗了,这个是一个很头疼的问题,你要是真想要保证每一帧都处理的话,就必须保证处理过程耗时33ms之内,即保证队列一定为空,否则时间长了队列会爆掉。另一个可能的方法是把数据写在硬盘里,不过你算算需要的写入速度也是很可观的,这就要自己取舍了。谢谢回复,现在情况是这样的,我有两种选择,1:将onpreviewframe返回的nv21数据存放在list中后续一并处理,这种方法肯定不掉帧,但缺点是内存开销太大,因为nv21数据格式本身太大。2:对传来的每帧实时处理,例如转成bitmap(格式更小)再存放在list中,后续再一起转成video.这种方法就会导致掉帧,因为转bitmap挺耗时间。几点疑问:1.您说mediacodec是将一个list的图像转成video对吧,应该不能写在onpreviewframe中做实时处理吧。2.将nv21转bitmap很耗时,需要0.1左右,所以掉帧,您知道有没有什么快速转换的方法吗?哪怕转成其他格式也行,因为这步唯一的目的就是减少list的内存消耗,后续转视频不需要实时。万分感谢!
现在问题是如果不对nv21进行转换压缩,100帧的视频会耗掉300M内存,我又不太想将分辨率设置太低。如果不做处理不考虑内存消耗的话,那我直接将这些帧放进list就好了,因为转视频我不需要实时。请问在这种情况下用队列的作用和优点是什么?能找到个同行真幸运,哈哈
你是说ArrayList?主要是没有concurrent支持。java.util.concurrent.LinkedBlockingQueue的优点在于一是线程安全;二是因为队列只会从头取出从尾放入,适合链表结构,避免了数组接口动态扩展的性能问题。
我觉得队列好处就是简单可控,不复杂也没有额外的开销,用着方便罢了。
Android对每个APP的内存是有上限的,队列爆了APP就自动崩溃了,我比较倾向于给队列设个最大容量,这样就算丢帧也能保证APP不崩溃;还有就是注意不要内存泄漏咯,让GC抓紧回收。
但如果不做数据压缩和处理,也不考虑内存消耗,把每一帧直接放进一个List中后续再转成视频不是更直接吗?相比用List的话,用队列能帮助减少内存消耗么?
恩,谢谢!博主的回复真的是神速。
博主,你好!我又来问个小问题了,我在理解了你博客的基础上进行了实时预览帧处理,但发现存在一个问题,我存储了预览帧图像,将它与自定义相机拍得的图像进行对比,发现二者差距很大,预览和图片分辨率我设的都一样,其他参数也一样。评论发不了对比图片,我在CSDN上也提了这个问题。http://ask.csdn.net/questions/350770
我不清楚是不是preview data和picture data是不是本身就不同,还是预览时需要指定一些参数?因为一个是preview,另一个是take picture,两者调用的API不同。预览帧可以理解为拍视频,一般可以支持到30fps,分辨率最大1080P;但拍照的话是调用的拍照API,不是onPreviewFrame(),没有API提供像onPreviewFrame()那样自动拍照的功能(Camera2可能可以),支持的最大fps可能只有10+,分辨率可以达到4K。
你打开相机就是开启了预览,这时预览帧全部显示在屏幕上;你点击拍照就是调用API专门拍照,不是预览帧咯。
博主,您好。想请教下,现在有需求要在扫描过程中得到分辨率很大的帧数据,应该怎么做呢?
这个就是把预览帧分辨率调到最大了,在开始相机预览前用getSupportedPreviewSizes()拿到支持的最大分辨率,然后再set到相机就好了,可以参考之前讲偏好设置的文章。
如果你是想得到像拍照那么高的分辨率的话,单纯的预览是做不到的,那你就要试试预览时调用API拍照了,我不知道能不能行~
如果把扫描过程当作在持续拍照,这种方式可行吗?
如果你对分辨率没有过高的要求(预览一般最大1080P,拍照一般最大4K),当然可以把预览的过程当作在持续拍照(可以说是1秒钟拍30张照片)。