Android: как отображать предварительный просмотр камеры с помощью обратного вызова?

Сначала я работал над этим некоторое время и пробовал несколько решений, поэтому не отправляйте мне первую ссылку, которую вы найдете в google.

Что мне нужно сделать, это довольно просто, я хочу вручную отобразить предварительный просмотр с камеры с помощью обратного вызова камеры, и я хочу получить по крайней мере 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. Предварительный просмотр камеры на SurfaceView (GL) с использованием NDK для обработки данных YUV. Я нашел решение здесь, в котором используется некоторая функция C и NDK. Но я не смог использовать его, здесь несколько подробнее. Но в любом случае это решение выполняется, чтобы вернуть ByteBuffer, чтобы отобразить его как текстуру в OpenGL, и это будет не быстрее, чем предыдущая попытка. Поэтому мне пришлось бы изменить его, чтобы возвратить массив int [], который можно нарисовать с помощью canvas.drawBitmap(), но я не понимаю C достаточно для этого.


Итак, есть ли какой-либо другой способ, который мне не хватает или какое-то улучшение в попытках, которые я пробовал?

Спасибо за любой ответ!

Ответ 1

Я работаю над одной и той же проблемой, но не совсем доволен.

Рассматривали ли вы рисование пикселей непосредственно на холсте без предварительного кодирования их в формате 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 для каждого кадра), но это может быть начало. Мне бы хотелось посмотреть, работает ли это для вас или нет - я все еще борюсь с проблемами, ортогональными вашим в настоящий момент.

Ответ 2

Я думаю, что Майкл на правильном пути. Сначала вы можете попробовать этот метод для преобразования из 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 в оттенках серого и должен работать и для ваших нужд.

Ответ 3

Разве это не то, что вы хотите? Просто используйте SurfaceView в своем макете, а затем где-то в вашем init, как onResume():

SurfaceView surfaceView = ...
SurfaceHolder holder = surfaceView.getHolder();
...
Camera camera = ...;
camera.setPreviewDisplay(holder);

Он просто отправляет кадры прямо в представление так быстро, как они поступают.

Если вы хотите оттенки серого, измените параметры камеры с помощью setColorEffect("mono").

Ответ 4

Для очень простых и простых эффектов есть

Camera.Parameters parameters = mCamera.getParameters();
parameters.setColorEffect(Parameters.EFFECT_AQUA);

Я понял, что эти эффекты различаются в зависимости от устройства. Например, на моем телефоне (Galaxy s II) это выглядит как комический эффект, так как в отличие от галактики s 1 это "просто" синий оттенок.

Это про: он работает как предварительный просмотр в прямом эфире.

Я просмотрел некоторые другие приложения для камеры, и они, очевидно, также столкнулись с этой проблемой. Так что же они сделали? Они захватывают изображение камеры по умолчанию, применяют фильтр к данным растрового изображения и показывают это изображение в простом ImageView. Это точно не так круто, как в режиме предварительного просмотра, но вы никогда не столкнетесь с проблемами производительности.

Ответ 5

Я считаю, что прочитал в блоге что данные в градациях серого находятся в первых байтах x * y. Yuv должен представлять яркость, поэтому данные есть, хотя это не идеальный оттенок серого. Он отлично подходит для относительной яркости, но не в оттенках серого, поскольку каждый цвет не такой яркий, как один из других в rgb. Зелену обычно дают более сильный вес в преобразованиях светимости. Надеюсь, это поможет!

Ответ 6

Есть ли какая-то особая причина, по которой вы вынуждены использовать GLES 1.0?

Потому что, если нет, см. принятый ответ здесь: Android SDK: получите изображение с предварительным просмотром камеры без его отображения

Обычно упоминается использование Camera.setPreviewTexture() в сочетании с GLES 2.0. В GLES 2.0 вы можете отображать полноэкранный квадрат по всему экрану и создавать любой эффект, который вы хотите.

Скорее всего, это самый быстрый способ.