
В предыдущей части мы убедились, что из 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. Следите за новыми выпусками!
Книги по теме: