- PVSM.RU - https://www.pvsm.ru -

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

Если вы пишете код для обработки изображений на С++, вы наверняка используете замечательную библиотеку OpenCV [1]. Уверен, вам не раз хотелось посмотреть на изображения в процессе отладки вашего кода. Для этого можно использовать такие удобные функции как imshow [2] или imwrite [3]. Однако это требует модификации исходного кода, а любая современная IDE во время отладки позволяет смотреть значения переменных на лету. Вот было бы здорово так же смотреть изображения?

Если в качестве IDE вы пользуетесь Visual Studio, то знаете, что с .NET [4] в этом плане всё [5] проще [6]. Однако речь идёт про OpenCV, а это только native C++, только хардкор. В этой статье я расскажу, как всё-таки заставить Visual Studio показывать изображения прямо в процессе отладки и дам ссылку на готовое решение. А также коротко расскажу о способах кастомизации Visual Studio.

Введение

Для тех, кто просто хочет воспользоваться готовым решением, сразу приведу ссылки:

Остальным я кратко расскажу об инструментах, с помощью которых можно в принципе кастомизировать Visual Studio, и более подробно о реализации решения одним из них. Все эксперименты выполнялись в Visual Studio 2010, готовое решение было протестировано в Visual Studio 2005, 2008, 2010, 2012 (и по идее должно работать в 2003).

Поиск готового решения

Когда мне по работе потребовалось отлаживать код на С++, использующий OpenCV для работы с изображениями, мне сразу же захотелось уметь смотреть их непосредственно во время отладки. Я радостно полез в гугл, и начал искать готовые решения. И… ничего не нашёл. Ни для изображений OpenCV, ни для вообще визуализации чего-либо из нативного C++ кода. Я поинтересовался у общественности [10], на что внушающий доверие резидент [11] мне сказал [12], что отладчик нативного кода в принципе не может быть расширен подобным образом. В этот момент я уверился, что велосипед ещё не изобретён.

Возможности кастомизации Visual Studio

Чтобы выбрать подходящий инструмент для визуализатора, нужно хотя бы приблизительно представлять, с каких сторон вообще можно расширять Visual Studio. Для начала нам понадобится знать два понятия:

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

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

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

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

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

С:Program Files (x86)Microsoft Visual Studio <vs_version>Common7PackagesDebuggerautoexp.dat

В нём описаны правила, в соответствии с которыми отображается содержимое переменных при отладке нативного C++ кода. Именно он заставляет, например, содержимое контейнера std::vector выглядеть так, как слева, а не так, как справа:

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

И вот как он это делает:

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

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

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

Небольшое руководство по написанию библиотек для файла autoexp.dat доступно в MSDN [25], там они его называют 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.

Форматирование строки отображения

Ну, раз уж мы всё равно здесь, давайте действительно отформатируем строчку, отображаемую отладчиком. А то стандартные внутренности выглядят не очень красиво. Сделаем что-нибудь типа такого:

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

Визуализация изображения

Собственно, ради чего мы все здесь и собрались. Раз уж теперь у нас есть изображение, можно делать визуализацию с помощью .NET. Я использовал старую добрую Windows Forms и C#. Форма с диалогом для визуализации находится в отдельной сборке, ей в конструкторе передаётся объект System.Bitmap. Чтобы его сконструировать, оригинальная dll собиралась с поддержкой .NET, с ключом /clr. Это позволило использовать C++/CLI.

Я переживал, что вызов .NET-диалога из нативной библиотеки вызовет сложности и придётся самому создавать AppDomain. Однако, поскольку студия сама является .NET-приложением, ничего такого делать не пришлось. Всё сразу заработало, ну я и не стал разбираться подробнее. Отмечу только, что пришлось показывать диалог из отдельной нити с параметром ApartmentState::STA, иначе возникали проблемы при открытии дополнительных диалогов, например для сохранения изображения на диск.

Теперь мы столкнулись с главным недостатком выбранного подхода — невозможно определить, вызывается функция при форматировании вывода в окно Watch, или при наведении пользователем мыши на переменную в редакторе. Если каждый раз показывать окно с изображением, этим невозможно будет пользоваться.

Единственное адекватное решение, которое я смог придумать, таково: окно показывается только в том случае, если нажата какая-либо спец-клавиша. А именно, Ctrl. В противном случае только форматируется строка вывода. Таким образом, чтобы посмотреть изображение в процессе отладки, пользователю надо зажать Ctrl, навести на переменную в редакторе и тогда выскочит окно. Кривовато, да, но по факту пользоваться оказалось достаточно удобно.

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

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

Интеграция с Visual Studio

Решение есть, теперь его надо упаковать. Для этого замечательно подходит существующий механизм расширений. Изначально я ориентировался на Visual Studio 2010, так что выбрал именно более современные расширения, а не более универсальные дополнения.

Создание расширений хорошо документировано [26]. Для начала, нам понадобится Visual Studio SDK [27] (для 2010 или 2012). После установки, можно будет создать новый проект Visual C#→Extensibility→Visual Studio Package. Запустится мастер, который спросит информацию о расширении:

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

По умолчанию он предлагает создать команду меню (Menu Comand), панель инструментов (Tool Window) или подкрутить редактор (Custom Editor). Нам не понадобится ничего из этого. Дело в том, что расширение представляет из себя файл VSIX, который является просто zip-архивом. При установке в Visual Studio, она распаковывает его в одну из пользовательских директорий (подробнее здесь [28]). Там могут быть библиотеки, взаимодействующие с MEF, но могут быть и любые другие файлы. Например, наша библиотека с Expression Evaluator Add-In.

Возникает небольшая проблема с загрузкой сборки с формой для визуализации. Если добавить её в References, то её поиск будет происходить в каталоге с исполнимым файлом devenv.exe и в Assembly::LoadFrom [29]. Диалог затем создаётся и показывается с помощью reflection [30]-методов.

Мне хотелось сделать настройки, которые можно было бы менять из меню Tools→Options. Это хорошо описано в руководстве в MSDN [31]. Там описано, как создавать стандартную сетку ключ/значение, и произвольную форму:

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

Просмотр изображений OpenCV во время отладки C++ кода в Visual Studio

Последняя форма понадобится для автоматического добавления строчки в файл autoexp.dat. К сожалению, не существует возможности выполнить какую-либо команду в момент установки расширения VSIX (см. здесь [32] таблицу Supported Capabilities). Поэтому после установки расширения, пользователю нужно зайти на эту страницу и нажать кнопку «Add entry».

Чтобы найти файл autoexp.dat, нужно знать, где установлена Visual Studio. Это можно посмотреть в реестре, но лучше спросить у неё самой с помощью DTE. Как получить объект DTE из расширения написано здесь [33]. Свойство FullName вернёт полный путь к файлу devenv.exe.

Приятно удивила Visual Studio 2012. В ней наконец-то предложена замена морально устаревшему файлу autoexp.dat — технология NATVIS [34]. Документации как таковой пока нет, но есть хорошее описание [35] в блоге Microsoft. И, слава обратной совместимости, они оставили возможность вызова стороннего кода через тот же механизм, только теперь он указывается в аргументе LegacyAddin. Единственное описание, что я нашёл, в ответе к этому вопросу [36]. Чувствуется, что технология совсем недавно анонсирована.

Огромный плюс NATIVS заключается в том, что теперь правила визуализации (оформленные в виде отдельных XML-файлов с расширением natvis) могут быть раскиданы по разным директориям. В том числе по пользовательским, а также они могут содержаться в расширениях. Для этого .nativs-файл достаточно добавить в Assets при сборке расширения. Поэтому в расширении NativeViewer для Visual Studio 2012 нет страницы с интеграцией и оно работает из коробки.

По поводу отладки VSIX-расширений. Это оказалось реализовано очень удобно. При запуске VSIX-проекта открывается экспериментальная копия Visual Studio, в которую установлена текущая версия расширения. Отладка работает нормально, мне удавалось отлаживать в исходной студии нативный код в dll, которую грузила экспериментальная копия.

Так как расширения появились только в Visual Studio 2010, на более ранние версии их придётся устанавливать вручную. Пока я написал руководство [37], а по-хорошему, конечно, следует сделать инсталлятор.

Заключение

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

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

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

Ссылки

Автор: mmatrosov

Источник [42]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-3/21967

Ссылки в тексте:

[1] OpenCV: http://opencv.willowgarage.com/wiki/

[2] imshow: http://docs.opencv.org/modules/highgui/doc/user_interface.html#imshow

[3] imwrite: http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html#imwrite

[4] .NET: http://code.google.com/p/opencvsharp/

[5] всё: http://code.google.com/p/opencvsharp/wiki/DebuggerVisualizer

[6] проще: http://victorhurdugaci.com/projects/vsimagevisualizer/

[7] NativeViewer на SourceForge: https://sourceforge.net/projects/nativeviewer/

[8] NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery: http://visualstudiogallery.msdn.microsoft.com/657956e4-8e02-4764-8022-72a0c9cc5b17

[9] NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery: http://visualstudiogallery.msdn.microsoft.com/26a8aecf-03b0-49ff-b691-41844c52f20e

[10] поинтересовался у общественности: http://stackoverflow.com/questions/9080283/debugging-unmanaged-c-images-in-visual-studio

[11] внушающий доверие резидент: http://stackoverflow.com/users/17034/hans-passant

[12] сказал: http://stackoverflow.com/questions/9080283/debugging-unmanaged-c-images-in-visual-studio#comment11400896_9080283

[13] DTE: http://msdn.microsoft.com/en-us/library/envdte.dte(v=vs.100).aspx

[14] MEF: http://mef.codeplex.com/

[15] Макросы (Macros): http://msdn.microsoft.com/en-us/library/b4c73967(v=vs.100).aspx

[16] Дополнения (Add-Ins): http://msdn.microsoft.com/en-us/library/5abkeks7(v=vs.100).aspx

[17] Расширения (Extensions): http://msdn.microsoft.com/en-us/library/dd885119(v=vs.100).aspx

[18] тут: http://keyvan.io/visual-studio-addin-vs-integration-package-part-4

[19] Визуализаторы (Visualizers): http://msdn.microsoft.com/en-us/library/zayyhzts(v=vs.100).aspx

[20] Debugger: http://msdn.microsoft.com/en-us/library/envdte.debugger(v=vs.100).aspx

[21] ExecuteStatement: http://msdn.microsoft.com/en-us/library/envdte.debugger.executestatement(v=vs.100).aspx

[22] GetExpression: http://msdn.microsoft.com/en-us/library/envdte.debugger.getexpression(v=vs.100).aspx

[23] здесь: http://www.idigitalhouse.com/Blog/?p=83

[24] CGAL: http://www.cgal.org/

[25] доступно в MSDN: http://msdn.microsoft.com/en-us/library/8fwk67y3%28v=vs.90%29.aspx

[26] хорошо документировано: http://blogs.msdn.com/b/visualstudio/archive/2010/02/19/how-vsix-extensions-are-discovered-and-loaded-in-vs-2010.aspx

[27] Visual Studio SDK: http://www.microsoft.com/en-us/download/details.aspx?id=2680

[28] здесь: http://blogs.msdn.com/b/visualstudio/archive/2009/12/07/bootstrapping-of-vs-packages-and-vsix-extensions-in-vs2010.aspx

[29] Assembly::LoadFrom: http://msdn.microsoft.com/en-us/library/1009fa28(v=vs.100).aspx

[30] reflection: http://msdn.microsoft.com/en-us/library/f7ykdhsy.aspx

[31] руководстве в MSDN: http://msdn.microsoft.com/en-us/library/bb166195

[32] здесь: http://msdn.microsoft.com/en-us/library/dd393694(v=vs.100).aspx

[33] здесь: http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/c3291548-8100-4256-85b9-10871800317a/#FAQAnswer13

[34] NATVIS: http://blogs.msdn.com/b/mgoldin/archive/2012/06/06/visual-studio-2012-and-debugger-natvis-files-what-can-i-do-with-them.aspx

[35] описание: http://code.msdn.microsoft.com/windowsdesktop/Writing-type-visualizers-2eae77a2

[36] этому вопросу: http://stackoverflow.com/questions/11545418/how-to-write-a-custom-native-visualizer-dll-for-visual-studio-2012-debugger

[37] руководство: https://sourceforge.net/p/nativeviewer/wiki/QuickStartGuide_VSold/

[38] Debug in Visual Studio using NativeViewer: http://code.opencv.org/projects/opencv/wiki/Debug_in_Visual_Studio_using_NativeViewer

[39] Anatomy of a VSIX Package: http://msdn.microsoft.com/en-us/library/dd997148(VS.100).aspx

[40] Building and publishing an extension for Visual Studio 2010: http://blogs.msdn.com/b/visualstudio/archive/2009/12/09/building-and-publishing-an-extension-for-visual-studio-2010.aspx

[41] VSX FAQ: http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/0be62d3d-84b2-4e17-a306-bcc460621192

[42] Источник: http://habrahabr.ru/post/161605/