Android: как отобразить предварительный просмотр камеры с обратным вызовом?
Что мне нужно сделать, это довольно просто, я хочу вручную отобразить предварительный просмотр с камеры с помощью обратного вызова камеры, и я хочу получить как минимум 15 кадров в секунду на реальном устройстве. Мне даже не нужны цвета, мне просто нужно предварительно просмотреть изображение в градациях серого. Изображения с камеры в формате YUV, и вы должны как-то обрабатывать их, что является основной проблемой производительности. Я использую API 8.
Во всех случаях я использую camera.setPreviewCallbackWithBuffer(), это быстрее, чем camera.setPreviewCallback(). Кажется, что я не могу получить около 24 кадров в секунду, если я не отображаю предварительный просмотр. Так что проблема не в этом.
Я пробовал эти решения:
1. Отобразите предварительный просмотр камеры в SurfaceView в виде растрового изображения. Это работает, но производительность составляет около 6 кадров в секунду.
baos = new ByteOutputStream();
yuvimage=new YuvImage(cameraFrame, ImageFormat.NV21, prevX, prevY, null);
yuvimage.compressToJpeg(new Rect(0, 0, prevX, prevY), 80, baos);
jdata = baos.toByteArray();
bmp = BitmapFactory.decodeByteArray(jdata, 0, jdata.length); // Convert to Bitmap, this is the main issue, it takes a lot of time
canvas.drawBitmap(bmp , 0, 0, paint);
2. Отобразите предварительный просмотр камеры в GLSurfaceView в качестве текстуры. Здесь я отображал только данные о яркости (изображение в градациях серого), что довольно просто, для каждого кадра требуется только одна arraycopy(). Я могу получить около 12 кадров в секунду, но мне нужно применить некоторые фильтры к предварительному просмотру, и кажется, что это не может быть сделано быстро в OpenGL ES 1. Поэтому я не могу использовать это решение. Некоторые подробности об этом в другом вопросе.
3. Отобразите предварительный просмотр камеры в (GL)SurfaceView, используя NDK для обработки данных YUV. Я нахожу здесь решение, которое использует некоторую функцию C и NDK. Но мне не удалось его использовать, вот еще некоторые подробности. Но в любом случае, это решение сделано для возврата ByteBuffer для отображения его в качестве текстуры в OpenGL, и он не будет быстрее, чем предыдущая попытка. Поэтому мне нужно изменить его так, чтобы он возвращал массив int[], который можно нарисовать с помощью canvas.drawBitmap(), но я недостаточно понимаю C, чтобы сделать это.
Итак, есть ли какой-то другой способ, который я пропускаю или какие-то улучшения в попытках, которые я пробовал?
6 ответов
Я работаю над точно такой же проблемой, но не так далеко, как вы.
Рассматривали ли вы рисование пикселей непосредственно на холсте без предварительного кодирования их в JPEG? Внутри набора OpenCV http://sourceforge.net/projects/opencvlibrary/files/opencv-android/2.3.1/OpenCV-2.3.1-android-bin.tar.bz2/download (который на самом деле не использует opencv; не волнуйтесь), есть проект под названием tutorial-0-androidcamera, который демонстрирует преобразование пикселей YUV в RGB и запись их непосредственно в растровое изображение.
Соответствующий код по существу:
public void onPreviewFrame(byte[] data, Camera camera, int width, int height) {
int frameSize = width*height;
int[] rgba = new int[frameSize+1];
// Convert YUV to RGB
for (int i = 0; i < height; i++)
for (int j = 0; j < width; j++) {
int y = (0xff & ((int) data[i * width + j]));
int u = (0xff & ((int) data[frameSize + (i >> 1) * width + (j & ~1) + 0]));
int v = (0xff & ((int) data[frameSize + (i >> 1) * width + (j & ~1) + 1]));
y = y < 16 ? 16 : y;
int r = Math.round(1.164f * (y - 16) + 1.596f * (v - 128));
int g = Math.round(1.164f * (y - 16) - 0.813f * (v - 128) - 0.391f * (u - 128));
int b = Math.round(1.164f * (y - 16) + 2.018f * (u - 128));
r = r < 0 ? 0 : (r > 255 ? 255 : r);
g = g < 0 ? 0 : (g > 255 ? 255 : g);
b = b < 0 ? 0 : (b > 255 ? 255 : b);
rgba[i * width + j] = 0xff000000 + (b << 16) + (g << 8) + r;
}
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.setPixels(rgba, 0/* offset */, width /* stride */, 0, 0, width, height);
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
canvas.drawBitmap(bmp, (canvas.getWidth() - width) / 2, (canvas.getHeight() - height) / 2, null);
mHolder.unlockCanvasAndPost(canvas);
} else {
Log.w(TAG, "Canvas is null!");
}
bmp.recycle();
}
Конечно, вам придется адаптировать его под свои нужды (например, не распределять rgba для каждого кадра), но это может быть началом. Я хотел бы посмотреть, работает ли это для вас или нет - я все еще борюсь с проблемами, ортогональными вашим в данный момент.
Я думаю, что Майкл на правильном пути. Сначала вы можете попробовать этот метод для преобразования из RGB в оттенки серого. Понятно, что он делает почти то же самое, что и его, но более лаконично для того, что вы хотите.
//YUV Space to Greyscale
static public void YUVtoGrayScale(int[] rgb, byte[] yuv420sp, int width, int height){
final int frameSize = width * height;
for (int pix = 0; pix < frameSize; pix++){
int pixVal = (0xff & ((int) yuv420sp[pix])) - 16;
if (pixVal < 0) pixVal = 0;
if (pixVal > 255) pixVal = 255;
rgb[pix] = 0xff000000 | (pixVal << 16) | (pixVal << 8) | pixVal;
}
}
}
Во-вторых, не создавайте кучу работы для сборщика мусора. Ваши растровые изображения и массивы будут иметь фиксированный размер. Создайте их один раз, а не в onFramePreview.
В результате вы получите что-то похожее на это:
public PreviewCallback callback = new PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if ( (mSelectView == null) || !inPreview )
return;
if (mSelectView.mBitmap == null)
{
//initialize SelectView bitmaps, arrays, etc
//mSelectView.mBitmap = Bitmap.createBitmap(mSelectView.mImageWidth, mSelectView.mImageHeight, Bitmap.Config.RGB_565);
//etc
}
//Pass Image Data to SelectView
System.arraycopy(data, 0, mSelectView.mYUVData, 0, data.length);
mSelectView.invalidate();
}
};
И тогда холст, куда вы хотите поместить, выглядит так:
class SelectView extends View {
Bitmap mBitmap;
Bitmap croppedView;
byte[] mYUVData;
int[] mRGBData;
int mImageHeight;
int mImageWidth;
public SelectView(Context context){
super(context);
mBitmap = null;
croppedView = null;
}
@Override
protected void onDraw(Canvas canvas){
if (mBitmap != null)
{
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
// Convert from YUV to Greyscale
YUVtoGrayScale(mRGBData, mYUVData, mImageWidth, mImageHeight);
mBitmap.setPixels(mRGBData, 0, mImageWidth, 0, 0, mImageWidth, mImageHeight);
Rect crop = new Rect(180, 220, 290, 400);
Rect dst = new Rect(0, 0, canvasWidth, (int)(canvasHeight/2));
canvas.drawBitmap(mBitmap, crop, dst, null);
}
super.onDraw(canvas);
}
Этот пример показывает обрезанный и искаженный выбор предварительного просмотра камеры в реальном времени, но вы поняли идею. Он работает на высоких FPS на Nexus S в оттенках серого и должен работать для ваших нужд.
Для очень простых и простых эффектов, есть
Camera.Parameters parameters = mCamera.getParameters();
parameters.setColorEffect(Parameters.EFFECT_AQUA);
Я понял, что эти эффекты работают по-разному в зависимости от устройства. Например, на моем телефоне (galaxy s II) это выглядит как комический эффект, так как в отличие от galaxy s 1 это просто синий оттенок.
Это профессионал: он работает в режиме live-preview.
Я посмотрел вокруг некоторых других приложений камеры, и они, очевидно, также столкнулись с этой проблемой. Так что они сделали? Они захватывают изображение с камеры по умолчанию, применяют фильтр к растровым данным и показывают это изображение в простом ImageView. Это точно не так круто, как в режиме предварительного просмотра, но вы никогда не столкнетесь с проблемами производительности.
Разве это не то, что вы хотите? Просто используйте SurfaceView в вашем макете, а затем где-нибудь в вашем init, как onResume()
:
SurfaceView surfaceView = ...
SurfaceHolder holder = surfaceView.getHolder();
...
Camera camera = ...;
camera.setPreviewDisplay(holder);
Он просто отправляет кадры прямо на экран так быстро, как они прибывают.
Если вы хотите использовать оттенки серого, измените параметры камеры с помощью setColorEffect("mono")
,
Я полагаю, что прочитал в блоге, что данные в градациях серого находятся в первых x*y байтах. Yuv должен представлять яркость, поэтому данные есть, хотя они не являются идеальными оттенками серого. Он отлично подходит для относительной яркости, но не для оттенков серого, так как каждый цвет не такой яркий, как в RGB. Зеленым обычно дается более сильный вес в преобразованиях светимости. Надеюсь это поможет!
Есть ли какая-то особая причина, по которой вы вынуждены использовать GLES 1.0?
Потому что, если нет, посмотрите принятый ответ здесь: Android SDK: получите необработанный предварительный просмотр изображения камеры без его отображения.
Обычно упоминается использование Camera.setPreviewTexture() в сочетании с GLES 2.0. В GLES 2.0 вы можете визуализировать полноэкранный четырехугольник по всему экрану и создать любой эффект, какой захотите.
Скорее всего, это самый быстрый способ.