[Из песочницы] Переписываем с java на C++ на платформе Android
Хочу с вами поделиться опытом переписывания с java на C++ на платформе Android и тем, что в результате получилось.
Для своего маленького домашнего проекта был использован алгоритм поиска лиц Виола-Джонса, java-исходники с моделью были взяты отсюда code.google.com/p/jviolajones с небольшой модификацией — были добавлены два класса: Point и Rectangle. Уточню почему не стал использовать OpenCV под Android — для его работы необходимо поставить отдельно приложение-библиотеку, что в моем случае будет весьма неудобно, и опыты показали его падение без предупреждения, разбираться с этим долго не стал, также и с поиском других библиотек, и решил взять простейшую готовую реализацию.
Быстродействие алгоритма показало плачевные результаты, на фотографии размером 400 на 300 на моем стареньком разбитом GT-I9300I — 54 секунды, на avd (виртуальном устройстве) и то дольше — 250 секунд.
Частенько попадались под мой взор обсуждения быстродействия кода на java и C++, где-то показывалось, что java отстает, в каких-то случаях даже и наоборот, приводились небольшие участки кода с одним циклом. Здесь же алгоритм чуть более, чем сложнее, порядка 6 вложенных циклов, как вы можете убедиться по исходникам. Поэтому было принято решение — испробовать на собственном опыте переписывание на C++. По всем прочитанным статьям у меня сложилось впечатление, что скорость повысится максимум процентов на 20, но как оказалось это было неверно.
Естественно встали следующие задачи — как передать входные и получить выходные данные и как переписать код. Заполнение модели из xml в конструкторе Detector решил оставить на java, заполняется, конечно, не быстро, но ввиду того, что работа с xml на C++ для меня звучит очень страшно, то оставил как есть. Род моей профессиональной деятельности связан с java, с C\C++ связывался только в институте и немного на работе по старым проектам. Поэтому пришлось изучить немного документации, почитать статьи и набить немного шишек.
Переписывание логики. Здесь особых проблем не возникло, был принят способ — не глядя скопировать классы, там, где eclipse подсвечивал красненьким проходился топориком. Все ArrayList'ы переделал в массив, благо — размер они не меняли.
Не буду описывать настройку среды для вызова нативного кода, статей на эту тему есть куча.
Как передать данные. С простыми типами — int, float, boolean все просто и понятно. С одномерным вроде бы тоже просто:
JNIEXPORT jint JNICALL Java_com_example_Computations_intFromJni(JNIEnv* env, jobject thiz, jintArray arr) {
jsize d = env->GetArrayLength(arr);
jboolean j;
int * p = env->GetIntArrayElements(arr, &j);
...
}
С двумерным чуть посложнее:
JNIEXPORT jint JNICALL Java_com_example_Computations_findFaces(JNIEnv* env, jobject thiz, jobjectArray image) {
int width = env -> GetArrayLength(image);
jboolean j2;
jintArray dim= (jintArray)env->GetObjectArrayElement(image, 0);
int height = env -> GetArrayLength(dim);
int **imageLocal;
imageLocal = new int*[width];
for (int i = 0; i < width; i++) {
jintArray oneDim= (jintArray)env->GetObjectArrayElement(image, i);
int *element = env->GetIntArrayElements(oneDim, &j2);
imageLocal[i] = new int[height];
for(int j=0; j < height; ++j) {
imageLocal[i][j]= element[j];
}
}
...
}
Поехали дальше, как передать объекты, у которых куча полей, среди которых есть типы List. Для получение поля объекта применяется следующая конструкция:
jclass clsDetector = env->GetObjectClass(objDetector);
jfieldID sizeFieldId = env->GetFieldID(clsDetector, "size", "Ldetection/Point;");
jobject pointObj = env->GetObjectField(objDetector, sizeFieldId);
Для листов нам понадобятся два метода get и size:
jfieldID stagesFieldId = env->GetFieldID(clsDetector, "stages", "Ljava/util/List;");
jobject stagesList = env->GetObjectField(detectorJObj, stagesFieldId);
jclass listClass = env->FindClass( "java/util/List" );
jmethodID getMethodIDList = env->GetMethodID( listClass, "get", "(I)Ljava/lang/Object;" );
jmethodID sizeMethodIDList = env->GetMethodID( listClass, "size", "()I" );
int listStagesCount = (int)env->CallIntMethod( stagesList, sizeMethodIDList );
for( int i=0; i < listStagesCount; ++i )
{
jobject stage = env->CallObjectMethod( stagesList, getMethodIDList, i);
...
Данные научились получать. Запускаем, валится на ошибке — Local reference table overflow 512 entries. Получается, что необходимо чистить все локальные ссылки jclass и jobject, это делается так:
env->DeleteLocalRef(jcls);
env->DeleteLocalRef(jobj);
И для массивов тоже:
env->ReleaseIntArrayElements(oneDim, element, JNI_ABORT);
env->DeleteLocalRef(oneDim);
Возвращаем результат. Для упрощения своей задачи возвращение результата сделал в виде массива Rectangle:
jclass cls = env->FindClass("detection/Rectangle");
jobjectArray jobAr =env->NewObjectArray(faces->currIndex, cls, NULL);
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIII)V");
for (int i = 0; i < faces->currIndex; i++) {
Rectangle* re = faces->rects[i];
jobject object = env->NewObject(cls, constructor, re->x, re->y, re->width, re->height);
env->SetObjectArrayElement(jobAr, i, object);
}
return jobAr;
Итак, торжественный момент — поиск на той же фотографии — 14 секунд, т.е. в 4 раза быстрее, на других фотографиях аналогичные результаты. На виртуальном андроиде 132 секунды против 300 секунд. Но как нам известно, нельзя использовать результаты одного опыта, необходимо повторить несколько раз, приведу для одной фотографии, время обработка в секундах.
Виртуальное устройство | Виртуальное устройство с использованием cpp | Мой телефон galaxy | Мой телефон galaxy с cpp |
---|---|---|---|
238 | 132 | 84 | 14 |
318 | 137 | 54 | 14 |
472 | 135 | 54 | 14 |
264 | 150 | 54 | 14 |
266 | 138 | 54 | 14 |
262 | 129 | 53 | 14 |
И в заключение отмечу. Несмотря на то, что переписывание дало большое ускорение, предела совершенству еще куча, можно использовать многопоточность, что я планирую изучить в ближайшее время. И внесение каких-либо корректировок в алгоритм — здесь наверное самая сложная часть.