|
|
|||||||||||||||||||||||||||||
|
Просмотр изображений OpenCV во время отладки C++ кода в Visual StudioИсточник: habrahabr mmatrosov
Если вы пишете код для обработки изображений на С++, вы наверняка используете замечательную библиотеку OpenCV. Уверен, вам не раз хотелось посмотреть на изображения в процессе отладки вашего кода. Для этого можно использовать такие удобные функции как imshow или imwrite. Однако это требует модификации исходного кода, а любая современная IDE во время отладки позволяет смотреть значения переменных на лету. Вот было бы здорово так же смотреть изображения? Если в качестве IDE вы пользуетесь Visual Studio, то знаете, что с .NET в этом плане всё проще. Однако речь идёт про OpenCV, а это только native C++, только хардкор. В этой статье я расскажу, как всё-таки заставить Visual Studio показывать изображения прямо в процессе отладки и дам ссылку на готовое решение. А также коротко расскажу о способах кастомизации Visual Studio. ВведениеДля тех, кто просто хочет воспользоваться готовым решением, сразу приведу ссылки:
Остальным я кратко расскажу об инструментах, с помощью которых можно в принципе кастомизировать Visual Studio, и более подробно о реализации решения одним из них. Все эксперименты выполнялись в Visual Studio 2010, готовое решение было протестировано в Visual Studio 2005, 2008, 2010, 2012 (и по идее должно работать в 2003).
Поиск готового решенияКогда мне по работе потребовалось отлаживать код на С++, использующий OpenCV для работы с изображениями, мне сразу же захотелось уметь смотреть их непосредственно во время отладки. Я радостно полез в гугл, и начал искать готовые решения. И… ничего не нашёл. Ни для изображений OpenCV, ни для вообще визуализации чего-либо из нативного C++ кода. Я поинтересовался у общественности, на что внушающий доверие резидент мне сказал, что отладчик нативного кода в принципе не может быть расширен подобным образом. В этот момент я уверился, что велосипед ещё не изобретён.
Возможности кастомизации Visual StudioЧтобы выбрать подходящий инструмент для визуализатора, нужно хотя бы приблизительно представлять, с каких сторон вообще можно расширять Visual Studio. Для начала нам понадобится знать два понятия:
Далее представлен список сущностей, используемых для кастомизации Visual Studio. Список составлен с учётом конкретной задачи, поэтому не претендует на полноту.
Казалось бы, визуализаторы - именно то, что нужно. Но вот незадача - работают они только с managed C++. Первые три варианта (макросы, дополнения и расширения) могут получить доступ к памяти отлаживаемого процесса только с помощью объекта Debugger, у которого из подходящих функций есть только ExecuteStatement и GetExpression. Однако обе они так или иначе возвращают результаты в виде строк, ограниченных по размеру. Конечно, можно постараться по кусочкам выдрать содержимое изображения и перевести назад в бинарный вид, но получается как-то очень криво. Существует ещё такой волшебный файлик под названием autoexp.dat. Он устанавливается вместе с Visual Studio и обычно лежит по адресу
В нём описаны правила, в соответствии с которыми отображается содержимое переменных при отладке нативного C++ кода. Именно он заставляет, например, содержимое контейнера std::vector выглядеть так, как слева, а не так, как справа:
И вот как он это делает:
Правило вывода std::vector в autoexp.dat
;------------------------------------------------------------------------------ ; std::vector from <vector> ;------------------------------------------------------------------------------ ; vector is previewed with "[<size>](<elements>)". ; It has [size] and [capacity] children, followed by its elements. ; The other containers follow its example. std::vector<*>{ preview ( #( "[", $e._Mylast - $e._Myfirst, "](", #array( expr: $e._Myfirst[$i], size: $e._Mylast - $e._Myfirst ), ")" ) ) children ( #( #([size] : $e._Mylast - $e._Myfirst), #([capacity] : $e._Myend - $e._Myfirst), #array( expr: $e._Myfirst[$i], size: $e._Mylast - $e._Myfirst ) ) ) } std::_Vector_iterator<*>/std::_Vector_const_iterator<*>{ preview ( *$e._Ptr ) children ( #([ptr] : $e._Ptr) ) } Это старая, плохо документированная технология. Поддерживается ещё начиная с Visual C++ 6.0. Небольшое пособие по использование здесь. Однако так вышло, что именно она Оказывается, что синтаксис файла autoexp.dat позволяет не только писать выражения для, например, адресной арифметики, но и вообще вызывать любой сторонний код! Это делается с помощью специальной конструкции $ADDIN. За неимением лучшего, именно этим инструментом я и воспользовался для своего визуализатора.
Решение на базе Expression Evaluator Add-InНебольшое руководство по написанию библиотек для файла autoexp.dat доступно в MSDN, там они его называют Expression Evaluator Add-In. Для вызова библиотечной функции достаточно указать путь к .dll-файлу и имя вызываемой функции.
Прототип функции в dll выглядит следующим образом:
В аргументе pHelper передаётся указатель на структуру DEBUGHELPER, которая предоставляет функции для обращения к памяти отлаживаемого процесса и возвращает адрес объекта, отображаемого в отладчике:
Код структуры DEBUGHELPER
В pResult нужно сохранить строку, показываемую отладчиком. По идее, это всё, что должна делать данная функция. Но кто же нас остановит? Доступ к памяти у нас уже есть. А дальше, как говорится, дело техники.
Извлечение содержимого изображенияЕдинственная информация о типе, доступная нам в данный момент - это его название, cv::Mat. Это значит, что придётся вычислить адреса полей на основе .h-файла какой-то конкретной версии OpenCV. Однако не думаю, что в дальнейшем структура полей этого фундаментального класса будет изменяться. Я не буду подробно останавливаться на технических моментах, типа как считать объект из памяти зная его адрес и имея .h-файл с его описанием, исходники выложены на SourceForge. Отмечу только, что структура pHelper позволяет узнать битность отлаживаемого процесса. А это значит, что нужно учесть, что указатели могут иметь размер как 4, так и 8 байт. Когда мы считали все поля объекта cv::Mat, мы можем точно так же считать содержимое самого изображения, т.к. его адрес находится в поле data.
Форматирование строки отображенияНу, раз уж мы всё равно здесь, давайте действительно отформатируем строчку, отображаемую отладчиком. А то стандартные внутренности выглядят не очень красиво. Сделаем что-нибудь типа такого:
Визуализация изображенияСобственно, ради чего мы все здесь и собрались. Раз уж теперь у нас есть изображение, можно делать визуализацию с помощью .NET. Я использовал старую добрую Windows Forms и C#. Форма с диалогом для визуализации находится в отдельной сборке, ей в конструкторе передаётся объект System.Bitmap. Чтобы его сконструировать, оригинальная dll собиралась с поддержкой .NET, с ключом /clr. Это позволило использовать C++/CLI. Я переживал, что вызов .NET-диалога из нативной библиотеки вызовет сложности и придётся самому создавать AppDomain. Однако, поскольку студия сама является .NET-приложением, ничего такого делать не пришлось. Всё сразу заработало, ну я и не стал разбираться подробнее. Отмечу только, что пришлось показывать диалог из отдельной нити с параметром ApartmentState::STA, иначе возникали проблемы при открытии дополнительных диалогов, например для сохранения изображения на диск. Теперь мы столкнулись с главным недостатком выбранного подхода - невозможно определить, вызывается функция при форматировании вывода в окно Watch, или при наведении пользователем мыши на переменную в редакторе. Если каждый раз показывать окно с изображением, этим невозможно будет пользоваться. Единственное адекватное решение, которое я смог придумать, таково: окно показывается только в том случае, если нажата какая-либо спец-клавиша. А именно, Ctrl. В противном случае только форматируется строка вывода. Таким образом, чтобы посмотреть изображение в процессе отладки, пользователю надо зажать Ctrl, навести на переменную в редакторе и тогда выскочит окно. Кривовато, да, но по факту пользоваться оказалось достаточно удобно.
При показе окна с изображением процесс Visual Studio блокируется, т.к. формально мы находимся в функции форматирования вывода отладчика. Это неприятно, но ничего фатального в этом нет, т.к. нет нужды одновременно изучать изображение и продолжать взаимодействовать со студией.
Интеграция с Visual StudioРешение есть, теперь его надо упаковать. Для этого замечательно подходит существующий механизм расширений. Изначально я ориентировался на Visual Studio 2010, так что выбрал именно более современные расширения, а не более универсальные дополнения. Создание расширений хорошо документировано. Для начала, нам понадобитсяVisual Studio SDK (для 2010 или 2012). После установки, можно будет создать новый проект Visual C#→Extensibility→Visual Studio Package. Запустится мастер, который спросит информацию о расширении:
По умолчанию он предлагает создать команду меню (Menu Comand), панель инструментов (Tool Window) или подкрутить редактор (Custom Editor). Нам не понадобится ничего из этого. Дело в том, что расширение представляет из себя файл VSIX, который является просто zip-архивом. При установке в Visual Studio, она распаковывает его в одну из пользовательских директорий (подробнеездесь). Там могут быть библиотеки, взаимодействующие с MEF, но могут быть и любые другие файлы. Например, наша библиотека с Expression Evaluator Add-In. Возникает небольшая проблема с загрузкой сборки с формой для визуализации. Если добавить её в References, то её поиск будет происходить в каталоге с исполнимым файлом devenv.exe и в GAC. Она же находится в директории с расширением, поэтому её приходится грузить вручную при помощи функцииAssembly::LoadFrom. Диалог затем создаётся и показывается с помощьюreflection-методов. Мне хотелось сделать настройки, которые можно было бы менять из меню Tools→Options. Это хорошо описано в руководстве в MSDN. Там описано, как создавать стандартную сетку ключ/значение, и произвольную форму:
Последняя форма понадобится для автоматического добавления строчки в файл autoexp.dat. К сожалению, не существует возможности выполнить какую-либо команду в момент установки расширения VSIX (см. здесь таблицу Supported Capabilities). Поэтому после установки расширения, пользователю нужно зайти на эту страницу и нажать кнопку "Add entry". Чтобы найти файл autoexp.dat, нужно знать, где установлена Visual Studio. Это можно посмотреть в реестре, но лучше спросить у неё самой с помощью DTE. Как получить объект DTE из расширения написано здесь. Свойство FullName вернёт полный путь к файлу devenv.exe. Приятно удивила Visual Studio 2012. В ней наконец-то предложена замена морально устаревшему файлу autoexp.dat - технология NATVIS. Документации как таковой пока нет, но есть хорошее описание в блоге Microsoft. И, слава обратной совместимости, они оставили возможность вызова стороннего кода через тот же механизм, только теперь он указывается в аргументе LegacyAddin. Единственное описание, что я нашёл, в ответе к этому вопросу. Чувствуется, что технология совсем недавно анонсирована. Огромный плюс NATIVS заключается в том, что теперь правила визуализации (оформленные в виде отдельных XML-файлов с расширением natvis) могут быть раскиданы по разным директориям. В том числе по пользовательским, а также они могут содержаться в расширениях. Для этого .nativs-файл достаточно добавить в Assets при сборке расширения. Поэтому в расширении NativeViewer для Visual Studio 2012 нет страницы с интеграцией и оно работает из коробки. По поводу отладки VSIX-расширений. Это оказалось реализовано очень удобно. При запуске VSIX-проекта открывается экспериментальная копия Visual Studio, в которую установлена текущая версия расширения. Отладка работает нормально, мне удавалось отлаживать в исходной студии нативный код в dll, которую грузила экспериментальная копия. Так как расширения появились только в Visual Studio 2010, на более ранние версии их придётся устанавливать вручную. Пока я написал руководство, а по-хорошему, конечно, следует сделать инсталлятор.
ЗаключениеВ этой статье я описал решение проблемы, с которой столкнулся в процессе разработки. Примечателен тот факт, что оно предназначено для популярных инструментов разработки, но, как ни странно, не имеет аналогов. Ну, по крайней мере я не смог найти ничего похожего. В этой статье я постарался меньше акцентировать внимание на технических деталях и компенсировать это обилием ссылок. Интересующиеся могут смотреть исходный код и задавать вопросы. Если какой-то из аспектов покажется общественности достаточно интересным, я могу написать о нём более подробную статью. Я очень надеюсь, что это расширение многим окажется полезным. Если кто-то хочет принять участие в развитии проекта - пишите. Ссылки по теме
|
|