Для своего маленького домашнего проекта был использован алгоритм поиска лиц Виола-Джонса, 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 |
И в заключение отмечу. Несмотря на то, что переписывание дало большое ускорение, предела совершенству еще куча, можно использовать многопоточность, что я планирую изучить в ближайшее время. И внесение каких-либо корректировок в алгоритм — здесь наверное самая сложная часть.
update Выложил исходники. Использовать следующим образом:
Detector detector = Detector.create(inputHaas);
List<Rectangle> res = detector.getFaces(background_image, 1.2f, 1.1f, .05f, 2, true, useCpp);
inputHaas — поток модели, т.е. файл haarcascade_frontalface_default.xml из исходного алгоритма, useCpp — использовать C++ или нет. В исходниках C++ не делаю освобождение памяти, т.к. писал на скорую руку.
Комментарии (15)
Dimezis
12.10.2015 16:54Мне кажется, что это все равно очень плохой результат и стоит смотреть в первую очередь реализацию алгоритма, без привязки к языку.
Что сразу бросилось в глаза — использование двумерного массива для работы с изображением. Это сильный удар по производительности и я считаю, что в таких алгоритмах следует использовать одномерную структуру.trolleg
12.10.2015 21:33+1Имеется ввиду использовать вместо array[i][j] вот тако array[i * width + j]?
DarthVictor
13.10.2015 11:02Да, в Java array[i][j] — это именно массив массивов. Который будет создавать N+1 массив для матрицы размерности N. В Си это, насколько я знаю, это не так принципиально.
Dimezis
13.10.2015 13:53Именно так. В Android, например, все битмапы внутри содержат одномерный массив пикселей.
Вы будете удивлены, на сколько быстрее обрабатывается одномерное представление.
Вообще, метод Виолы-Джонса рассчитан на использование в реал тайме, так что вы должны понимать, что ваша реализация уж очень сильно проседает по производительности.
Suvitruf
12.10.2015 17:37+4Не первый раз уже вижу статьи о том как люди «переписывают Java алгоритмы на C++», при этом говоря, что алгоритм ускорился в 5, в 10, в 100 раз. Вот только я ни в одной этой статье не наблюдал, чтобы автор профилировал исходный Java код.
«На Java алгоритм медленно работает, перепишу-ка я его на C++» — не правильный подход. Как по мне, разумнее найти проблемы именно в Java части, быть может их довольно просто пофиксить.dyadyaSerezha
13.10.2015 14:53Да, наверное. С другой стороны, если есть внутренняя неэффективность самого алгоритма, то она перейдет и в С++ версию. Хотя, например, вынести все локальные reference-переменные методов выше в класс надо было, конечно (исходники не смотрел, но это обычно очень большой замедлитель сложных численных алгоритмов на Джаве).
0leGG
14.10.2015 03:40Я вот прямо чувствую, что переписав парсер XML (на использование того же SAX), уже можно догнать и перегнать C++. Не говоря уж о том, что и как можно сделать, если оптимизировать java-код.
trolleg
14.10.2015 17:38Да, парсер здесь не быстрый, омобенно это заметно на виртуальном устройстве. Парсер в замере скорости не участвовал, предполагается, что он один раз инициализируется.
VasiliyKudryavtsev
Интересно, конечно, но необходимость переписывания на C++ по статье оценить сложно.
Вы профилировали Java-версию? Не было видно узких мест?
На эмуляторе с Android 5 и средой Dalvik не запускали?
И, если, можно покажите исходники C++ и Java версии
QtRoS
Средой ART имелось ввиду?
VasiliyKudryavtsev
Да, конечно
trolleg
Исходники приложил в конце статьи. Сначала хотел сделать профилирование, но «на глаз» не обнаружил узких мест, но, думаю, что займусь этим, т.к. самому очень интересно. Для эмуляции использовался Android 4.3.1, ART не использовал. Боевое устройство на Android 4.3.