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