среда, 18 сентября 2013 г.

Несколько советов по Java биндингам для openCV

Большим достоинством OpenCV являются её автоматически генерируемые биндинги для Java и Python. Биндинги есть пока не для всего, но они покрывают большую часть функционала библиотеки, и, что не мало важно, автоматически дополняются при правильной реализации новых алгоритмов.

В Python биндингах в качестве базовой библиотеки используется NumPy и все его примитивы. В Java с этим всё сложнее. Подходящих примитивов не нашлось, поэтому для простого оперирования с объектами OpenCV были реализованы легковесные Java обёртки, которые просто хранят указатель на нативный объект и оборачивают все его методы с учётом Java специфики. Вот несколько мыслей о том как с этим жить.

1. Свои Jni обёртки для  алгоритмов. Часто бывает необходимо реализовать свой алгоритм с использованием OpenCV, а потом использовать его из Java или обернуть в Java уже существующие чужие наработки. Всё это особенно актуально для Android приложений. Работа идёт гладко, до того момента, пока не появляется необходимость передать OpenCV'шный объект, например Mat, из Java в нативный C/C++ код. Здесь многие встают в ступор и не знают что делать. И это вполне объяснимо, потому что большая часть Jni  кода генерируется автоматически и в релизный SDK для Android не попадает. Те небольшие части, которые написаны руками, можно найти только в Git репозитории проекта глубоко в структуре каталогов в modules/java/generator/src.
А решение задачи очень простое. Все Java классы для OpenCV имеют публичное поле nativeObj типа long, которое хранит адрес плюсового объекта в памяти, а все методы реализованы примерно так:

// Java
class MyProcessor
{
    public static void process(Mat img)
    {
         native_process(img.nativeObj);
    }
    private static native native_process(long mat_addr);
};

// C/C++
JNIEXPORT void JNICALL xxx_native_process(JNIEnv * jenv, jclass, jlong mat)
{
    MyProcessing(*img);}

2. Исключения и ассерты С/С++. OpenCV достаточно активно использует исключения и ассерты, которые достаточно просто отлаживаются если у вас чисто нативное или чисто Java приложение, но создают много неприятностей, если у вас смешанный код. В таком случае сильно упрощает жизнь оборачивание тела нативных вызовов в try-catch, например так:

cv::Mat* img = (cv::Mat*)mat;
try
{
 

}
catch(cv::Exception& e)
{
    LOGD("Processing caught cv::Exception: %s", e.what());
    jclass je = jenv->FindClass("org/opencv/core/CvException");
    if(!je)
            je = jenv->FindClass("java/lang/Exception");
        jenv->ThrowNew(je, e.what());
}
catch (...)
{
    LOGD("Processing caught unknown exception");
    jclass je = jenv->FindClass("java/lang/Exception");
    jenv->ThrowNew(je, "Unknown exception in JNI code in Processing");

}

Такое управление исключениями в нативном коде позволяет быстро отлаживать ошибки с типами матриц, размерами буферов и прочими ассертами из библиотеки. Таким образом реализованы все методы библиотеки.

3. Управление памятью и Java объектами с нативной частью. По поводу связки Java <-> C/C++ написано много и по этой теме я ничего нового не скажу. Если вы выделяете в нативном коде память - будьте добры её удалить. Всё здесь хорошо, если вы вызываете соответствующие методы в Java, а не полагаетесь на сборщик мусора, который когда нибудь удалит ваш объект-обёртку для нативного кода и освободит все выделенные буферы вместе с ним (не сам конечно). И даже в втором случае всё неплохо, до тех пор пока нативный объект занимает мало места в памяти.
Чудеса начинаются когда нативные объекты становятся много больше, чем их Java обёртка. Примером тому может служить тот же самый Mat из OpenCV. Все привыкли к тому, что Mat имеет внутри счётчик ссылок и сам управляет память памятью. Java обёртка для него почти ничего не весит и освобождает нативный Mat при удалении объекта. Всё вроде неплохо, пока мы не начинаем массово создавать, а потом "бросать" Mat'ы. На первый взгляд всё ничего криминального: проснётся сборщик мусора, приберёт Java обёртки для Mat'a а в месте с ними удалится и нативный объект. Но этого, к сожалению, часто не происходит. Mat в куче Java машины занимает ничтожно мало места. Постоянное создание новых матов не приводит существенному использованию Java кучи. Сборщик мусора видит только вершину айсберга и не спешит ничего удалять. В итоге мы имеем периодические "утечки памяти" там где их и быть-то не должно. Если на настольных компьютерах с этим можно как-то худо бедно жить, то на Android это настоящая беда. Там памяти не так много и совершенно нет файла подкачки. Такие "утечки" иногда приводят к подвисанию или даже перезагрузке телефона. Общая мораль проста: зовите Release явно у всех мало-мальски больших объектов и объектов, которые могут на них ссылаться!