Просмотр изображений 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. Для начала нам понадобится знать два понятия:

  • DTE Интерфейс взаимодействия с Visual Studio, который был введен в .NET-версиях (начиная с 2003). С его помощью можно получить объекты, которые предоставляют доступ к самым разным частям Visual Studio - структуре решения, настройкам оболочки, активному окну, даже к отладчику.
  • MEF Managed Extensibility Framework - более новая модель, доступная начиная с Visual Studio 2010. Позволяет осуществлять более тесную интеграцию и обеспечивает взаимодействие MEF-компонент, которые в свою очередь могут использовать DTE.

Далее представлен список сущностей, используемых для кастомизации Visual Studio. Список составлен с учётом конкретной задачи, поэтому не претендует на полноту. 

  • Макросы (Macros) Отлично подходят для автоматизации каких-либо действий, выполняемых над исходным кодом, структурой проекта и т.п. С их помощью можно выполнить проверку стиля кода, сгенерировать шаблоны, записать и воспроизвести некоторые действия пользователя. Имеют доступ к DTE. Доступны в меню Tools→Macros.
  • Дополнения (Add-Ins) Появились начиная с Visual Studio .NET. Используются для любых мыслимых кастомизаций, от улучшенной подсветки синтаксиса, до интеграции системы контроля версий. Имеют доступ к DTE. Доступны через Tools→Add-In Manager.
  • Расширения (Extensions) Появились начиная с Visual Studio 2010, являются более продвинутой версией дополнений (список отличий тут). Взаимодействуют с MEF. Доступны через Tools→Extension Manager.
  • Визуализаторы (Visualizers) Предназначены для отображения объектов в процессе отладки. Доступны при нажатии маленькой иконки с лупой рядом с именем переменной при отображении её содержимого.

Казалось бы, визуализаторы - именно то, что нужно. Но вот незадача - работают они только с managed C++. Первые три варианта (макросы, дополнения и расширения) могут получить доступ к памяти отлаживаемого процесса только с помощью объекта Debugger, у которого из подходящих функций есть только ExecuteStatement и GetExpression. Однако обе они так или иначе возвращают результаты в виде строк, ограниченных по размеру. Конечно, можно постараться по кусочкам выдрать содержимое изображения и перевести назад в бинарный вид, но получается как-то очень криво.

Существует ещё такой волшебный файлик под названием autoexp.dat. Он устанавливается вместе с Visual Studio и обычно лежит по адресу 

С:\Program Files (x86)\Microsoft Visual Studio <vs_version>\Common7\Packages\Debugger\autoexp.dat

В нём описаны правила, в соответствии с которыми отображается содержимое переменных при отладке нативного 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. Небольшое пособие по использование здесь. Однако так вышло, что именно она спасёт отца русской демократии позволит нам получить нормальный доступ к памяти отлаживаемого процесса. Кроме того, если вы часто работаете со сложными многоуровневыми структурами (например, считаете геометрию с помощью CGAL), пара вручную добавленных правил в этом файле может серьёзно упростить вам жизнь.

Оказывается, что синтаксис файла autoexp.dat позволяет не только писать выражения для, например, адресной арифметики, но и вообще вызывать любой сторонний код! Это делается с помощью специальной конструкции $ADDIN. За неимением лучшего, именно этим инструментом я и воспользовался для своего визуализатора.

Решение на базе Expression Evaluator Add-In

Небольшое руководство по написанию библиотек для файла autoexp.dat доступно в MSDN, там они его называют Expression Evaluator Add-In. Для вызова библиотечной функции достаточно указать путь к .dll-файлу и имя вызываемой функции. 

cv::Mat=$ADDIN(NativeViewer.dll,CvMatViewer)

Прототип функции в dll выглядит следующим образом:

HRESULT WINAPI CvMatViewer(DWORD dwAddress, DEBUGHELPER* pHelper, 
  int nBase, BOOL bUniStrings, char* pResult, size_t max, DWORD reserved)

В аргументе pHelper передаётся указатель на структуру DEBUGHELPER, которая предоставляет функции для обращения к памяти отлаживаемого процесса и возвращает адрес объекта, отображаемого в отладчике:

Код структуры DEBUGHELPER
typedef struct tagDEBUGHELPER
{
    DWORD dwVersion;
    HRESULT (WINAPI *ReadDebuggeeMemory)( struct tagDEBUGHELPER *pThis, DWORD dwAddr, DWORD nWant, VOID* pWhere, DWORD *nGot );
    // from here only when dwVersion >= 0x20000
    DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis );
    HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr, DWORD nWant, VOID* pWhere, DWORD *nGot );
    int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis );
} 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, на более ранние версии их придётся устанавливать вручную. Пока я написал руководство, а по-хорошему, конечно, следует сделать инсталлятор.

Заключение

В этой статье я описал решение проблемы, с которой столкнулся в процессе разработки. Примечателен тот факт, что оно предназначено для популярных инструментов разработки, но, как ни странно, не имеет аналогов. Ну, по крайней мере я не смог найти ничего похожего. 

В этой статье я постарался меньше акцентировать внимание на технических деталях и компенсировать это обилием ссылок. Интересующиеся могут смотреть исходный код и задавать вопросы. Если какой-то из аспектов покажется общественности достаточно интересным, я могу написать о нём более подробную статью.

Я очень надеюсь, что это расширение многим окажется полезным. Если кто-то хочет принять участие в развитии проекта - пишите.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=31380