среда, 9 ноября 2011 г.

Как написать программу на C++ для Android.
Часть 3: Используем С++ класс и STL

Часть 1 | Часть 2 | Часть 3 | Часть 4 | Часть 5 (Mac OS)


В предыдущей части мы убедились, что из Java программы можно довольно просто вызывать C++ функции. В этой статье рассмотрим более сложный пример с C++ классами. На C++ будем считать статистику по картинке получаемой со встроенной видеокамеры устройства (насколько я знаю, все Android устройства имеют хотя бы одну видеокамеру).

Для начала встроим в наш проект вывод картинки с видеокамеры, а также получение видеокадров. Для этого нам нужно добавить View, который будет отображать картинку с камеры. View — это очередной важный элемент Android-программы. Можно считать, что Views — это прямоугольные области визуализации, которые можно включать на каждой Activity. Можно одновременно показывать несколько Views, что мы и сделаем. Один View будет на весь экран показывать видео, а второй поверх первого будет отображать гистограмму картинки. Для того, чтобы подключить View к Activity необходимо вызывать функцию setContentView.

Но для начала нам нужно создать класс для показа камеры. Для этого открываем файл TestActivity.java и следующий код выше класса TestActivity:
import java.io.IOException;

import android.app.Activity;
import android.os.Bundle;
import android.content.Context;
import android.hardware.Camera;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

class Preview extends SurfaceView implements SurfaceHolder.Callback {
    Camera camera_;
    boolean finished_;
    SurfaceHolder holder_;

    Preview(Context context) {
        super(context);

        finished_ = false;

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        holder_ = getHolder();
        holder_.addCallback(this);
        holder_.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        Camera.Parameters parameters = camera_.getParameters();
        parameters.setPreviewSize(320, 240);
        parameters.setPreviewFrameRate(25);
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
        camera_.setParameters(parameters);
        camera_.startPreview();
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
        // Because the CameraDevice object is not a shared resource, it's very
        // important to release it when the activity is paused.
        finished_ = true;
        camera_.setPreviewCallback(null);
        camera_.stopPreview();
        camera_.release();
        camera_ = null;
    }
    
    public void surfaceCreated(SurfaceHolder holder) {
        camera_ = Camera.open();
        try {
            camera_.setPreviewDisplay(holder);
        } 
        catch (IOException exception) {
            camera_.release();
            camera_ = null;
        }
    }    
}

Тут мы создаем свой View с тремя методами surfaceCreated, surfaceDestroyed и surfaceChanged. В первых двух создаем и удаляем объект Camera, в последнем инициализируем камеру. Для того, чтобы получить картинку с камеры в манифесте требуется добавить разрешение android.permission.CAMERA. Открываем файл AndroidManifest.xml и выбираем закладку Permissions. Там жмем кнопку Add и добавляем Uses Permission:


В поле Name пишем android.permission.CAMERA:


Итоговый файл манифеста будет выглядеть следующим образом (его можно посмотреть в закладке AndroidManifest.xml):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.blogspot.jia3ep.test"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.CAMERA"/>

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".TestActivity"
                  android:label="@string/app_name"
                  android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Я добавил туда параметр android:screenOrientation="landscape", чтобы картинка с камеры не была повернута в окне отображения. Можно запустить программу, чтобы убедиться, что видео показывает:


Теперь добавим код, который будет считать гистограмму картинки. В проект добавляем добавляем два файла — histogram.h и histogram.cpp:

Вводим имя файла:


Содержание файла histogram.h:
/*
 * histogram.h
 *
 *  Created on: 12.05.2011
 *      Author: Kirill V. Lyadvinsky (aka jia3ep)
 */

#pragma once

#include <vector>

class histogram
{
public:
  histogram();
  ~histogram();

  void init_from_YUV420SP( unsigned char* yuv420sp, int width, int height );
  void get_histograms( int* r, int* g, int* b ) const;
  int get_max_height() const { return max_height_; }
  int get_height() const { return height_; }
  int get_width() const { return width_; }

protected:
  int  width_;
  int  height_;
  mutable int max_height_;

  std::vector<int> rgbdata_;
};

Содержание файла histogram.cpp:
/*
 * histogram.cpp
 *
 *  Created on: 12.05.2011
 *      Author: Kirill V. Lyadvinsky (aka jia3ep)
 */

#include "histogram.h"

histogram::histogram() : width_(0), height_(0), max_height_(0)
{
}

void histogram::init_from_YUV420SP(unsigned char *yuv420sp, int width, int height)
{
  width_ = width;
  height_ = height;
  const int frame_size = width * height;
  rgbdata_.resize( frame_size );

  for ( int j = 0, yp = 0; j < height; j++ ) {
    int uvp = frame_size + ( j >> 1) * width, u = 0, v = 0;
    for ( int i = 0; i < width; i++, yp++ ) {
      int y = ( 0xFF & yuv420sp[yp] ) - 16;
      if ( y < 0 ) y = 0;
      if ( (i & 1) == 0 ) {
        v = ( 0xFF & yuv420sp[uvp++] ) - 128;
        u = ( 0xFF & yuv420sp[uvp++] ) - 128;
      }

      const int y1192 = 1192 * y;
      int r = ( y1192 + 1634 * v );
      if ( r < 0 ) r = 0; else if ( r > 252143 ) r = 262143;
      int g = ( y1192 - 833 * v - 400 * u );
      if ( g < 0 ) g = 0; else if ( g > 252143 ) g = 262143;
      int b = ( y1192 + 2066 * u );
      if ( b < 0 ) b = 0; else if ( b > 252143 ) b = 262143;

      rgbdata_[yp] = 0xFF000000 | ((r << 6) & 0xFF0000) | ((g >> 2) & 0xFF00) | ((b >> 10) & 0xFF);
    }
  }
}

void histogram::get_histograms(int *r, int *g, int *b) const
{
  for (int bin = 0; bin < 256; bin++)
  {
    r[bin] = g[bin] = b[bin] = 0;
  }
  max_height_ = 0;
  if ( rgbdata_.empty() ) return;

  for (int pix = 0; pix < width_*height_; pix += 3)
  {
    const int p = rgbdata_[pix];
    const int r_pixVal = (p >> 16) & 0xff;
    const int g_pixVal = (p >> 8) & 0xff;
    const int b_pixVal = p & 0xff;
    if ( r_pixVal > 10 && r_pixVal < 245 ) {
     r[r_pixVal]++;
     if ( r[r_pixVal] > max_height_ ) max_height_ = r[r_pixVal];
    }
    if ( g_pixVal > 10 && g_pixVal < 245 ) {
     g[g_pixVal]++;
     if ( g[g_pixVal] > max_height_ ) max_height_ = g[g_pixVal];
    }
    if ( b_pixVal > 10 && b_pixVal < 245 ) {
     b[b_pixVal]++;
     if ( b[b_pixVal] > max_height_ ) max_height_ = b[b_pixVal];
    }
  }
}

histogram::~histogram()
{
}

Как можно видеть, ничего особенного в этих файлах нет — класс инициализируем YUV данными, которые приходят с камеры, потом можем получить гистограмму по каждой компоненте. Можно заметить, что Eclipse ругается на включение заголовочного файла vector:


Это потому, что не знает где его искать. Я не знаю какой официальный путь решения этой проблемы, но мне помогло добавления пути к заголовочным файлам stlport (/home/user/Android/android-ndk-r6b/sources/cxx-stl/stlport/stlport). Как это сделать я писал в предыдущей статье.

Далее, чтобы добавить поддержку C++ Standard Library в проект, добавляем файл Application.mk таким же образом, как h и cpp файлы ранее. В него добавляем всего одну строку:
APP_STL   := gnustl_static

Для того, чтобы файл histogram.cpp попал в сборку, необходимо добавить его в список LOCAL_SRC_FILES в файле Android.mk. Также добавляем флаг LOCAL_ALLOW_UNDEFINED_SYMBOLS := true, чтобы избежать ошибок вида undefined reference to `std::__throw_length_error. А ошибки появятся, если использовать библиотеки, которые идут с NDK в скомпилированном виде. Мы пока используем именно их.

Почитать про прочие параметры Application.mk и Android.mk можно в документации, которая ставится вместе с NDK. По какой-то причине в онлайне её нет.

Чтобы использовать класс histogram в коде Java, в идеале, нужно написать отдельный прокси-класс на Java. В нашем случае, для упрощения и ускорения, добавим необходимые методы прямо в класс HistogramView, который будет рисовать график поверх видео. Добавляем:
public long histogram_cpp_;
public native void decodeYUV420SP( long cppobj, byte[] yuv, int width, int height);
private native void calculateHistogram(long cppobj, int[] r_histogram, int[] g_histogram, int[] b_histogram);
private native void doneCppSide( long cppobj );
private native long initCppSide();

Функция initCppSide возвращает указатель на созданный объект класса histogram. Другие функции принимают этот указатель и вызывают функции именно для этого экземпляра класса. Можно видеть, что ни о какой типизации речи не идет. Полную реализацию HistogramView можно посмотреть в архиве с полным проектом, ссылку на который можно найти в конце статьи.

Генерируем объявления функций также как делали это раньше:
cd ~/workspace/test/
javah -classpath .:bin/classes:/home/user/Android/android-sdk-linux_x86/platforms/android-8/android.jar -jni com.blogspot.jia3ep.test.HistogramView

В результате появляется файл com_blogspot_jia3ep_test_HistogramView.h. Реализацию этих методов добавляем в уже существующий test.cpp и не забываем добавить #include "../com_blogspot_jia3ep_test_HistogramView.h" и #include "histogram.h".

В классе TestActivity не забываем добавить слой с HistogramView:
setContentView( preview_ );
addContentView( histogram_view_, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) );

В результате получаем нарисованную поверх видео гистограмму:


Архив с полным набором исходных кодов, которые мы писали, качаем по этой ссылочке .

В следующей части попробуем написать Android приложение используя исключительно C++. Ага, без строчки на Java. Следите за новыми выпусками!

Книги по теме:

Комментировать в ВКонтакте