Summary
In this article, we will not introduce the new functions of Camera APP, but how to process the Camera preview frame data. Presumably most people don't need to deal with preview frames, because cameras only need to be used for taking photos and recording. In fact, this article doesn't have much connection with general Camera development, but it is still operating Camera class, so it is still classified as Camera development. To process preview frames is simply to process the data of each frame when the Camera previews. Generally speaking, if the sampling rate of the Camera is 30 FPS, there will be 30 frames of data to be processed in one second. What is the frame data? If you are rushing to deal with frame data, you must have known the answer for a long time. In fact, it is an array of byte type, which contains frame data in YUV format. This paper only introduces several efficient methods to deal with preview frame data, but not the specific use, because it is a long story to use for face recognition, image beautification and so on.
This article is in Android camera development (2): add preferences to the camera Based on the introduction. The processing of preview frame data usually involves a lot of calculation, which leads to the low efficiency of processing because of too much frame data, and the resulting preview picture stuck and other problems. This paper mainly introduces how to separate threads and optimize the screen display, and how to improve the efficiency of frame data processing by using HandlerThread, Queue, ThreadPool and AsyncTask.
Get ready
For the sake of simplicity, we start to get the preview frame and process it when the camera starts to preview. In order to analyze the process more clearly, we add "start" and "stop" buttons under the "Settings" button in the UI to control the start and stop of the camera preview.
Modify UI
Modify activity main.xml to
Java
<Button android:id="@+id/button_settings" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Set up" />
Replace with
Java
<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="Set up" /> <Button android:id="@+id/button_start_preview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="start" /> <Button android:id="@+id/button_stop_preview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Stop it" /> </LinearLayout>
In this way, two buttons "start" and "stop" are added.
Binding events
Modify mainActivity, and transfer the code of initializing camera preview in the original onCreate() to the new method startPreview()
Java
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(); } }); }
At the same time, a stopPreview() method is added to stop the camera preview
Java
public void stopPreview() { FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview); preview.removeAllViews(); }
stopPreview() gets the FrameLayout where the camera preview is located, and then removes the camera preview through removeAllViews(). At this time, the relevant end method in the CameraPreview class will be triggered to close the camera preview.
Now onCreate() is very simple, just bind two buttons to corresponding methods
Java
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(); } }); }
Try running
Now running APP will not start camera preview immediately. Click the "start" button to display the camera preview screen. Click "stop" to make the screen disappear and preview stop.
Basic frame data acquisition and processing
Here we first implement the most basic and commonly used method of frame data acquisition and processing; then we look at the methods to improve the performance.
Basics
The interface to get frame data is Camera.PreviewCallback. To achieve the onpreviewframe (byte [] data, Camera) method under this interface, you can get the data of each frame. So what we need to do now is to add the Camera.PreviewCallback interface declaration to the CameraPreview class, then implement the onPreviewFrame() method in CameraPreview, and finally bind the interface to Camera. In this way, for each preview frame generated during Camera preview, the onPreviewFrame() method will be called to process the preview frame data.
In CameraPreview, modify
Java
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback
by
Java
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback
Add Camera.PreviewCallback interface declaration.
Add the implementation of onPreviewFrame()
Java
public void onPreviewFrame(byte[] data, Camera camera) { Log.i(TAG, "processing frame"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }
Instead of processing frame data, we pause 0.5 seconds to simulate processing frame data.
Add the sentence getCameraInstance() in surfaceCreated()
Java
mCamera.setPreviewCallback(this);
Bind this interface to mCamera so that onPreviewFrame() is called whenever a preview frame is generated.
Try running
Now run the APP and click "start". Generally, no obvious difference can be observed on the screen, but there are actually two potential problems. First, if you click "Settings" at this time, you will find that the setting interface does not appear immediately, but will appear a few seconds later; and then click the back button, the setting interface will disappear in a few seconds. Second, you can see the output "processing frame" in logcat, which is about 0.5 seconds. Because the thread sleep is set to 0.5 seconds, only 2 frames of 30 frames per second are processed, and the remaining 28 frames are discarded (there is no very intuitive way to show that the remaining 28 frames are discarded, but the fact is that, not strictly speaking, when the new frame data arrives If onPreviewFrame() is executing and has not returned, the frame data will be discarded.
Separate from UI thread
problem analysis
Now let's solve the first problem. The first reason is simple, which is often encountered in Android Development: the UI thread is occupied, resulting in the UI operation stuck. In this case, onPreviewFrame() will block the thread, and the blocked thread is the UI thread.
On which thread does onPreviewFrame() execute? There are descriptions in the official documents:
Called as preview frames are displayed. This callback is invoked on the event thread open(int) was called from.
This means that onPreviewFrame() runs on the same thread as Camera.open(). At present, Camera.open() is executed in UI thread (because no new process has been created). The corresponding solution is simple: let Camera.open() execute in non UI thread.
Solution
Here we use HandlerThread to implement. HandlerThread will create a new thread with its own loop, so that the specified statement can be executed in the new thread through Handler.post(). Although it's easy to say, there are still some details to deal with.
Start with HandlerThread and add it to CameraPreview
Java
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 inherits from HandlerThread. Start the Thread in the constructor and create a handler. The effect of openCamera() is to execute mCamera = Camera.open(); in this Thread, so it is executed in Runnable() through handler.post(), and the statement to be executed is encapsulated in openCameraOriginal(). The use of notify wait is for security, because post() execution will return immediately, while Runnable() will execute asynchronously, which may still be null when using mmcamera immediately after post(); therefore, add the notify wait control here, and openCamera() will return only after confirming that the camera is turned on.
Next, openCameraOriginal() is added to CameraPreview
Java
private void openCameraOriginal() { try { mCamera = Camera.open(); } catch (Exception e) { Log.d(TAG, "camera is not available"); } }
This does not need to be explained, but encapsulation becomes a method.
Finally, change getCameraInstance() to
Java
public Camera getCameraInstance() { if (mCamera == null) { CameraHandlerThread mThread = new CameraHandlerThread("camera thread"); synchronized (mThread) { mThread.openCamera(); } } return mCamera; }
It's also easy to understand. It's for camera handler thread to handle.
Try running
Now running the APP, you will find that the first problem has been solved.
Processing frame data
Next, to solve the second problem, how to ensure that no frame data is discarded, that is, to ensure that every frame data is processed. The central idea of the solution is very clear: let onPreviewFrame() return as soon as possible without discarding the frame data.
Here are four common processing methods: HandlerThread, Queue, AsyncTask and ThreadPool. The advantages and disadvantages of each method are simply analyzed.
HandlerThread
brief introduction
Using HandlerThread is to use Android's Message Queue to process frame data asynchronously. The simple process is to encapsulate the frame data as Message when onPreviewFrame() is called and send it to HandlerThread. HandlerThread obtains Message in the new thread and processes the frame data. Because it takes a short time to send a Message, it will not cause frame data loss.
Realization
Create a new ProcessWithHandlerThread class with the content of
Java
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 inherits HandlerThread and Handler.Callback interface, which implements handleMessage() method to process the obtained Message. The frame data is encapsulated in the obj attribute of the Message and marked with what. processFrame() deals with frame data, for example only.
Next, instantiate ProcessWithHandlerThread, bind interface, encapsulate frame data and send Message in CameraPreview.
Add a new member variable to CameraPreview
Java
private static final int PROCESS_WITH_HANDLER_THREAD = 1; private int processType = PROCESS_WITH_HANDLER_THREAD; private ProcessWithHandlerThread processFrameHandlerThread; private Handler processFrameHandler;
Add at the end of the constructor
Java
switch (processType) { case PROCESS_WITH_HANDLER_THREAD: processFrameHandlerThread = new ProcessWithHandlerThread("process frame"); processFrameHandler = new Handler(processFrameHandlerThread.getLooper(), processFrameHandlerThread); break; }
Note that the new Handler() here is also binding the interface to have ProcessWithHandlerThread handle the received Message.
Modify onPreviewFrame() to
Java
public void onPreviewFrame(byte[] data, Camera camera) { switch (processType) { case PROCESS_WITH_HANDLER_THREAD: processFrameHandler.obtainMessage(ProcessWithHandlerThread.WHAT_PROCESS_FRAME, data).sendToTarget(); break; } }
Here, the frame data is encapsulated as Message and sent out.
Try running
Now run the APP, there will be a lot of "tests" in logcat, you can also modify processFrame() to test.
Analysis
This method is to flexibly apply Android's Handler mechanism and solve the problem with its Message Queue model, Message Queue. The problem is whether it will exceed the limit if all the frame data is encapsulated as Message and thrown to Message Queue. However, it has not been encountered yet. Another problem is that the Handler mechanism may be too large, which is not "lightweight" compared to dealing with this problem.
Queue
brief introduction
Queue method is to use queue to establish frame data queue. onPreviewFrame() is responsible for adding frame data to the end of the queue, while the processing method takes out frame data at the head of the queue and processes it. Queue is the role of buffering and providing interface.
Realization
Create a new ProcessWithQueue class with the content as
Java
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"); } }
When ProcessWithQueue is instantiated, the Queue is provided externally. In order to process frame data independently and at any time, ProcessWithQueue inherits Thread and overloads run() method. The dead loop in the run() method is used to process the frame data in the Queue at any time. mQueue.take() blocks when the Queue is empty, so it will not cause CPU occupation caused by the loop. processFrame() deals with frame data, for example only.
Next, create a queue in CameraPreview and instantiate ProcessWithQueue to add frame data to the queue.
Add a new member variable to CameraPreview
Java
private static final int PROCESS_WITH_QUEUE = 2; private ProcessWithQueue processFrameQueue; private LinkedBlockingQueue<byte[]> frameQueue;
take
Java
private int processType = PROCESS_WITH_THREAD_POOL;
Modified to
Java
private int processType = PROCESS_WITH_QUEUE;
Add to the switch of the constructor
Java
case PROCESS_WITH_QUEUE: frameQueue = new LinkedBlockingQueue<>(); processFrameQueue = new ProcessWithQueue(frameQueue); break;
The LinkedBlockingQueue is used to meet the concurrency requirements. Because only the head and tail of the team are operated, the linked list structure is used.
Add to the switch of onPreviewFrame()
Java
case PROCESS_WITH_QUEUE: try { frameQueue.put(data); } catch (InterruptedException e) { e.printStackTrace(); } break;
Add frame data to the end of the queue.
Try running
Now run the APP, there will be a lot of "tests" in logcat, you can also modify processFrame() to test.
Analysis
This method can be simply understood as a simplification of the previous HandlerThread method. It only uses LinkedBlockingQueue to realize buffering, and writes out the queue processing method itself. This method also does not avoid the disadvantages mentioned before. If the frame data in the queue cannot be processed in time, it will cause the queue to be too long and occupy a lot of memory. But the advantage is simple and convenient.
AsyncTask
brief introduction
The AsyncTask method uses the AsyncTask class of Android, which will not be described in detail here. In short, each time AsyncTask is called, an asynchronous processing event will be created to execute the specified method asynchronously. In this case, the normal frame data processing method will be handed over to AsyncTask for execution.
Realization
Create a new ProcessWithAsyncTask class with the content of
Java
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 inherits AsyncTask, overloads doInBackground() method, and enters byte [] to return String. The code in doInBackground() is executed asynchronously. Here is processFrame(), which processes frame data. Here is just an example.
Next, you need to instantiate ProcessWithAsyncTask in CameraPreview and give the frame data to AsyncTask. Unlike the previous method, a new ProcessWithAsyncTask is instantiated and executed every time new frame data is processed.
Add a new member variable to CameraPreview
Java
private static final int PROCESS_WITH_ASYNC_TASK = 3;
take
Java
private int processType = PROCESS_WITH_QUEUE;
Modified to
Java
private int processType = PROCESS_WITH_ASYNC_TASK;
Add to the switch of onPreviewFrame()
Java
case PROCESS_WITH_ASYNC_TASK: new ProcessWithAsyncTask().execute(data); break;
Instantiate a new ProcessWithAsyncTask, pass frame data to it and execute it.
Try running
Now run the APP, there will be a lot of "tests" in logcat, you can also modify processFrame() to test.
Analysis
This method is simple in code, but it is difficult to understand its underlying implementation. AsyncTask actually uses thread pool technology to realize asynchronous and concurrent. Compared with the previous method, it has the advantage of high concurrency, but it can not go on indefinitely, and it will be restricted by the frame processing time. In addition, according to the introduction in the official documents, the emergence of AsyncTask is mainly to solve the problem of UI thread communication, so it's a sideshow here. AsyncTask has less "master" than the previous methods, and may not meet some requirements.
ThreadPool
brief introduction
The ThreadPool method mainly uses the Java ThreadPoolExecutor class. Presumably, the previous AsyncTask is a bit more low-level. The concurrent processing of frame data is realized by setting up thread pool manually.
Realization
Create a new ProcessWithThreadPool class with the content of
Java
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"); } }
The ProcessWithThreadPool constructor creates a thread pool. The corePoolSize is the concurrency, which is the number of processor cores. The maximum poolsize of the thread pool is set to twice the concurrency. post() is used to execute the frame data processing method through the thread pool. processFrame() deals with frame data, for example only.
Next, we will instantiate ProcessWithThreadPool in CameraPreview and give the frame data to ThreadPool.
Add a new member variable to CameraPreview
Java
private static final int PROCESS_WITH_THREAD_POOL = 4; private ProcessWithThreadPool processFrameThreadPool;
take
Java
private int processType = PROCESS_WITH_ASYNC_TASK;
Modified to
Java
private int processType = PROCESS_WITH_THREAD_POOL;
Add to the switch of the constructor
Java
case PROCESS_WITH_THREAD_POOL: processFrameThreadPool = new ProcessWithThreadPool(); break;
Add to the switch of onPreviewFrame()
Java
case PROCESS_WITH_THREAD_POOL: processFrameThreadPool.post(data); break;
Give the frame data to ThreadPool.
Try running
Now run the APP, there will be a lot of "tests" in logcat, you can also modify processFrame() to test.
Analysis
ThreadPool method is clearer than AsyncTask code, which is not "mysterious", but the two ideas are the same. ThreadPool method has more customization space when establishing thread pool, but it also fails to avoid the disadvantages of AsyncTask method.
A little nagging
Many of the methods described above only describe the idea of processing, and need to be modified according to the actual use, but the process is generally like this. Due to the lack of perfect test methods for real-time processing, bug s often exist and need to be carefully checked. For example, if two or three frames are lost in the processed frames, it is difficult to find out. Even if it is found, it is not easy to find out the wrong methods, and a large number of tests are needed.
The above methods are summarized based on the countless pits I stepped on. Because I haven't found a high-quality article about real-time preview frame processing, I can contribute some of my knowledge to help people in need even if I can achieve the goal.
For the actual processing problems of frame data and YUV format, please refer to some articles I wrote earlier about Android video decoding and YUV format parsing, and hope to help you.
DEMO
The camera APP source code implemented in this article is on GitHub. If you need to click zhantong/AndroidCamera-ProcessFrames.
Reference resources
- 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 )'