При разработке чего нибудь полезного с OpenCV под Android часто возникает необходимость пробросить какие-то свои функции и классы в Java. Пока функций совсем чуть-чуть это можно сделать руками, но в какой-то момент количество рутинного кода начинает зашкаливать и лопается терпение. Проблема обостряется, когда в Java хочется завернуть целый модуль. При проявлении проблемы возникает закономерный вопрос: "Нельзя ли это автоматизировать и как это сделано в самой OpenCV?" Подробности под катом.
среда, 13 ноября 2013 г.
среда, 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 явно у всех мало-мальски больших объектов и объектов, которые могут на них ссылаться!
В 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 явно у всех мало-мальски больших объектов и объектов, которые могут на них ссылаться!
понедельник, 22 июля 2013 г.
Собираем OpenCV для Windows RT (ARM)
С выходом Windows 8 полку поддерживаемых Windows архитектур прибыло. Новичок в этой компании после x86, AMD64 и Itanium мобильная платформа на ARM-v7a. Недавний выпуск релиз OpenCV порадовал нас более полной поддержкой этой платформы и первым примером использования. Эта заметка - маленькое руководство о том как скомпилировать OpenCV для Windows RT на ARM.
Для счастья нам понадобится десктоп с Windows 8 и только с ней, семёрка не подойдёт. Далее нужны будут Visual Studio 2012, Windows Platform SDK и cmake. Кроме того, понадобится любой из билд инструментов nmake, make, ninja, что у вас есть и что вам по вкусу. Это минимальный набор инструментов. Полезно, но не обязательно иметь под рукой git, если захочется собрать актуальную версию из репозитория или внести свой вклад в библиотеку.
Как только инструмент готов - можно начинать. У меня в качестве билд утилиты ninja, но всё то же самое будут работать и с nmake и make, надо только поменять параметр -G у cmake'a. Ninja работает заметно быстрее, чем названные конкуренты и не имеет проблем с многопоточным билдом, что не может не радовать.
Открываем консоль разработчика и переходим в папочку с исходниками OpenCV. У меня это D:\Projects\OpenCV:
Далее создаём здесь билд каталог и переходим в него:
Когда билд каталог готов и окружение настроено, запускам cmake:
Далее запускаем make/nmake/ninja и ждём окончания билда. Я запускаю построение для цели install, чтобы потом было удобнее подключать OpenCV к своему проекту.
У меня все работает отлично и проходят все тесты, но не смотря на это есть некоторое количество нерешённых проблем. Главная из них - Win32 API, используемый в core и highgui. Хотя от highgui можно легко избавится, вопрос с core остаётся открытым. Работа над этой проблемой идёт. Будет наедятся, что следующий релиз библиотеки будет успешно проходить все необходимые проверки.
Для счастья нам понадобится десктоп с Windows 8 и только с ней, семёрка не подойдёт. Далее нужны будут Visual Studio 2012, Windows Platform SDK и cmake. Кроме того, понадобится любой из билд инструментов nmake, make, ninja, что у вас есть и что вам по вкусу. Это минимальный набор инструментов. Полезно, но не обязательно иметь под рукой git, если захочется собрать актуальную версию из репозитория или внести свой вклад в библиотеку.
Как только инструмент готов - можно начинать. У меня в качестве билд утилиты ninja, но всё то же самое будут работать и с nmake и make, надо только поменять параметр -G у cmake'a. Ninja работает заметно быстрее, чем названные конкуренты и не имеет проблем с многопоточным билдом, что не может не радовать.
Открываем консоль разработчика и переходим в папочку с исходниками OpenCV. У меня это D:\Projects\OpenCV:
d: cd Projects\OpenCVUpd: Обрашаю внимание, здесь на нужна Visual Studio Developer Console, а не обычный cmd.exe. В ней уже выставлены базовые параметры окружения.
Далее создаём здесь билд каталог и переходим в него:
mkdir build_winrt cd build_winrtВыставляем правильное окружение командой:
"C:\Program Files\Microsoft Visual Studio 11.0\VC\bin\x86_arm\vcvarsx86_arm.bat"Дело в том, что cmake пока (версия 2.8.11.1) не подружился с Windows RT, приходится обманывать его и выставлять платформу, компилятор и прочее не средствами cmake, а через переменные окружения.
Когда билд каталог готов и окружение настроено, запускам cmake:
cmake -GNinja -DWITH_TBB=ON -DBUILD_TBB=ON -DBUILD_opencv_gpu=OFF \ -DBUILD_opencv_python=OFF -DBUILD_opencv_java=OFF -DWITH_FFMPEG=OFF \ -DWITH_WIN32UI=OFF -DWITH_VFW=OFF -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_TOOLCHAIN_FILE=..\platforms\winrt\arm.winrt.toolchain.cmake ..Хотя я и сказал, что компилятор и платформа выставляются через переменные окружения, для удачной сборки нужны дополнительные флаги компилятору и линкеру, которые выставляются в псевдо-тулчейне.
Далее запускаем make/nmake/ninja и ждём окончания билда. Я запускаю построение для цели install, чтобы потом было удобнее подключать OpenCV к своему проекту.
ninja installСобственно всё! В директории install мы получили все необходимые заголовки и библиотеки необходимые для создания своего приложения с OpenCV.
У меня все работает отлично и проходят все тесты, но не смотря на это есть некоторое количество нерешённых проблем. Главная из них - Win32 API, используемый в core и highgui. Хотя от highgui можно легко избавится, вопрос с core остаётся открытым. Работа над этой проблемой идёт. Будет наедятся, что следующий релиз библиотеки будет успешно проходить все необходимые проверки.
суббота, 13 апреля 2013 г.
Макросы определяемые разными компиляторам
Недавно при поиске некоторых дефайнов, которые определяют GCC и MSVC наткнулся на полезнейший проект на Sourceforge. Ребята собирают таблицу стандартных макросов для разных С и С++ компиляторов. Очень пригодится при разработке любого кросс платформенного и кросс компиляторного проекта.
понедельник, 1 апреля 2013 г.
GCC -> Visual Studio: Особенности компиляции
Продолжение темы портирования на Windows RT. В этот раз несколько слов об особенностях компиляторов.
Не смотря на сплошную стандартизацию C и C++ перенести проект, который разрабатывался с использованием только одного компилятора, на другой компилятор, а тем более другую платформу, достаточно непростая задача. В этой заметке несколько слов о портировании Android проекта на Windows RT. Отдельно отмечу, что речь идёт о математической библиотеке и системных вопросов я касаться не буду.
Проект довольно давно рос и развивался исключительно для Android и успел обрасти большим количеством оптимизаций, в том числе с использованием векторного модуля NEON и распараллеливания с помощью TBB. Первым шагом на пути компиляции было решено отключить оба этих полезных инструмента. Пока не до жиру, надо собрать основной код и получить нужную функциональность. Благо для этого в коде были заранее заготовлены макросы.
Первым препятствием на пути к успешному билду встали суффиксы у констант типа long long unsinged int. Как выяснилось GCC понимает и LLU и ULL суффиксы, а компилятор от Microsoft варант LLU напрочь не переваривает.
Следующая проблема - inline/noinline аттрибуты функций. Рекомендация к включению тела функции в вызываемый код стандартизирована и одинакова у всех. Делается это ключевым словом inline в начале определения функции. Атрибут с обратной семантикой, т.е. не инлайнить функцию, не стандартизирован и у каждого свой. В GCC это __attribute__ ((noinline)) Visual Studio это __declspec(noinline). Для добавления нового компилятора имеет смысл добавить платформо зависимый макрос и использовать его в коде.
Интересная ситуация возникает с экспортом функций и классов. При компиляции GCC под Linux платформы обычно по умолчанию все функции и классы экспортируются из динамических библиотек. Это значит можно сильно не заморачиваться при написании тестов и звать всё что вам нужно. При переходе на Visual Studio картина меняется на противоположную. По умолчанию, всё, что не отмечено для экспорта в интерфейсе библиотеки, не видно. Что затрудняет тестирование приватных классов. Экспорт функций и классов так же не стандартизирован. В GCC используется атрибут
Не смотря на сплошную стандартизацию C и C++ перенести проект, который разрабатывался с использованием только одного компилятора, на другой компилятор, а тем более другую платформу, достаточно непростая задача. В этой заметке несколько слов о портировании Android проекта на Windows RT. Отдельно отмечу, что речь идёт о математической библиотеке и системных вопросов я касаться не буду.
Проект довольно давно рос и развивался исключительно для Android и успел обрасти большим количеством оптимизаций, в том числе с использованием векторного модуля NEON и распараллеливания с помощью TBB. Первым шагом на пути компиляции было решено отключить оба этих полезных инструмента. Пока не до жиру, надо собрать основной код и получить нужную функциональность. Благо для этого в коде были заранее заготовлены макросы.
Первым препятствием на пути к успешному билду встали суффиксы у констант типа long long unsinged int. Как выяснилось GCC понимает и LLU и ULL суффиксы, а компилятор от Microsoft варант LLU напрочь не переваривает.
Следующая проблема - inline/noinline аттрибуты функций. Рекомендация к включению тела функции в вызываемый код стандартизирована и одинакова у всех. Делается это ключевым словом inline в начале определения функции. Атрибут с обратной семантикой, т.е. не инлайнить функцию, не стандартизирован и у каждого свой. В GCC это __attribute__ ((noinline)) Visual Studio это __declspec(noinline). Для добавления нового компилятора имеет смысл добавить платформо зависимый макрос и использовать его в коде.
Интересная ситуация возникает с экспортом функций и классов. При компиляции GCC под Linux платформы обычно по умолчанию все функции и классы экспортируются из динамических библиотек. Это значит можно сильно не заморачиваться при написании тестов и звать всё что вам нужно. При переходе на Visual Studio картина меняется на противоположную. По умолчанию, всё, что не отмечено для экспорта в интерфейсе библиотеки, не видно. Что затрудняет тестирование приватных классов. Экспорт функций и классов так же не стандартизирован. В GCC используется атрибут
__attribute__ ((dllexport)), а в Visual Studio - __declspec(dllexport). Как и с инлайнингом, в таком случае разумно использовать свой макрос.
вторник, 19 марта 2013 г.
Портируем CMake проект на ARM Windows RT
CMake замечательная штука для создания действительно кросс платформенных проектов. В заметке несколько рецептов о том, как быстро портировать свой любимый проект на новую систему.
CMake из коробки умеет генерировать проекты для Visual Studio, и это предпочтительный способ сборки, если вы работаете на Windows. К большому сожалению, cmake до сих пор, даже в ночных сборках не научился опознавать компилятор под ARM и генерировать правильный проект. Ребята из Kitware активно работают над новой версией и обещают поддержку Windows RT к версии 2.8.11, а мы тем временем пойдём другим путём.
Быстрый пробег по поисковой выдаче Гугла даёт обсуждение на Stackoverflow. Начальная идея найдена. Можно генерировать не проект для Visual Studio, а Makefile для nmake. Достаточно правильно выставить переменные окружения. После такого фокуса CMake думает, что имеет дело со стандартным хостовым компилятором и ничего не знает про кросс компиляцию.
Конфигурация обычно проходит нормально, но запустив билд любого мало мальски серьёзного проекта мы получим сообщение об ошибке:
Причину провала таких тестов можно посмотреть в логах в директории CMakeFiles в билд каталоге вашего проекта. Для того, чтобы такие логи сохранялись в файл добавьте ключ --debug-trycompile в командную строку cmake'a при конфигурации. Макрос добавленный функцией add_definitions не попадает в список макросов при компиляции тестовых программ. Его надо добавить самостоятельно в переменную CMAKE_REQUIRED_DEFINITIONS:
Ninja работает намного быстрее своего штатного аналога и сразу распараллеливает билд на все доступные системе процессоры. Проект развивается как замена обычному Unix mak'у, работает на множестве платформ и вообще отличная штука!
CMake из коробки умеет генерировать проекты для Visual Studio, и это предпочтительный способ сборки, если вы работаете на Windows. К большому сожалению, cmake до сих пор, даже в ночных сборках не научился опознавать компилятор под ARM и генерировать правильный проект. Ребята из Kitware активно работают над новой версией и обещают поддержку Windows RT к версии 2.8.11, а мы тем временем пойдём другим путём.
Быстрый пробег по поисковой выдаче Гугла даёт обсуждение на Stackoverflow. Начальная идея найдена. Можно генерировать не проект для Visual Studio, а Makefile для nmake. Достаточно правильно выставить переменные окружения. После такого фокуса CMake думает, что имеет дело со стандартным хостовым компилятором и ничего не знает про кросс компиляцию.
Конфигурация обычно проходит нормально, но запустив билд любого мало мальски серьёзного проекта мы получим сообщение об ошибке:
C:\Program Files\Microsoft Visual Studio 11.0\VC\INCLUDE\crtdefs.h(338):Решение проблемы можно найти в crtdefs.h или в том же обсуждении. Достаточно добавить макрос ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE. Для cmake это лучше сделать функцией add_definitions. Он сам добавит макрос в нужном формате и там где надо:
fatal error C1189: #error: Compiling Desktop applications for the ARM platform is not supported.
project(MyFavouriteProject) add_definitions(ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE) ...В большинстве случаев добавление макроса решает все проблемы, но это не всё. Настоящие кросс платформенные проекты часто используют разные системные и не очень инклюды, которые могут отличаються от платформы к платформе. Для проверки наличия того или иного заголовочного файла в cmake используется функция check_include_file. При вызове этой функции cmake автоматически создаёт маленькую программу на C\С++ с подключенным заголовком и пытается её скомпилировать. Если билд прошёл успешно, значит заголовок есть и можно идти дальше. Но причиной падения может быть не только отсутствие заголовочного файла, но и та же беда в crtdefs.h, если это файл прямо или косвенно входит в тестовую программу.
Причину провала таких тестов можно посмотреть в логах в директории CMakeFiles в билд каталоге вашего проекта. Для того, чтобы такие логи сохранялись в файл добавьте ключ --debug-trycompile в командную строку cmake'a при конфигурации. Макрос добавленный функцией add_definitions не попадает в список макросов при компиляции тестовых программ. Его надо добавить самостоятельно в переменную CMAKE_REQUIRED_DEFINITIONS:
project(MyFavouriteProject) set(CMAKE_REQUIRED_DEFINITIONS -D_ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE) add_definitions(ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE) check_include_file(assert.h HAVE_ASSERT_H) ...И ещё один маленький рецепт напоследок. Вместо nmake можно использовать Ninja. Инструмент быстро и легко собирается из исходного кода. Правда для старта ему нужен Python. Такая полезная в хозяйств штука у большинства разработчиков и так есть, поэтому никакой сложности сборка составить не должна. После сборки складываем единственный получившийся исполняемый файл куда вам нравится и добавляем этот путь в PATH. После этого в командной строке cmake заменяем -G "NMake Makefiles" на -GNinja и зовём ninja вместо nmake.
Ninja работает намного быстрее своего штатного аналога и сразу распараллеливает билд на все доступные системе процессоры. Проект развивается как замена обычному Unix mak'у, работает на множестве платформ и вообще отличная штука!
воскресенье, 10 марта 2013 г.
GDB Седержимое ВСЕХ регистов на ARM архитектуре
Недавно отлаживая очень странное падение нативного кода на Android телефоне с процессором, поддерживающем NEON, наткнулся на интересную особенность. Отладчик gdb из состава NDK-r8d по команде info registers показывает не все регистры для арифметики с плавающей точкой. Если точнее, то в дампе только первые 16, как будь то NEON расширения нет совсем. Как выяснилось чуть позднее, он ведёт себя так на ARM-v7a всегда. Чтобы посмотреть дополнительные регистры, надо сказать в gdb консоли info all-registers.
Подписаться на:
Сообщения (Atom)