Начинаем реализацию: первые досадные огорчения Я был очень удивлен и расстроен, когда узнал, что в великом и могучем .NET Framework напрочь отсутствует возможность простого взаимодействия с веб-камерами. В четвертой версии ситуация улучшилась (для SilverLight-проектов точно появились соответствующие классы), но протестировать я их не успел, поскольку пример для данной статьи я начал писать еще до официального выхода VS2010 и 4-го .NET’a. Практически отчаявшись, я плотно засел в гугле. Результаты поиска по рунету меня не вдохновили – все, что я нашел – это ссылки на MSDN и технологию DirectDraw. Я даже попробовал набросать простенький примерчик, но из-за отсутствия опыта работы с DirectDraw меня постиг облом. У меня получилось собрать совсем простенькое приложение, но я так и не смог выловить в нем все глюки. Еще больше отчаявшись, я принялся шерстить ресурсы наших западных товарищей. Проштудировав несколько десятков ссылок, я смог нарыть много вкусностей. Среди них были всевозможные примеры и небольшие статейки (американцы не любят много писать). Мне даже удалось найти рабочий пример на основе DirectDraw, но, когда я увидел код – ужаснулся. Разобраться в нем было тяжело. Поэтому я решил с ним не заморачиваться, а попытаться найти способ попроще. Не успел я распрощаться с примером на DirectDraw, как на глаза мне попался еще один. Автор примера закодил целую библиотеку для работы с веб-камерами и другими устройствами видеозахвата, используя технологию VFW (Video For Windows). Жаль, что проект автора (я про библиотеку) был максимально кастрирован. Все, что позволяла сделать библиотека – вывести изображение с веб-камеры. Ни захвата отдельных кадров, ни записи видео и других полезных нам фич не было. И тем не менее, мое подсознание решительно сказало мне, что этот проект и есть то, что я искал. Не успел я беглым взглядом пробежаться по его коду, как увидел имена знакомых win-сообщений и не менее знакомых названий WinAPI функций. Когда-то давным-давно мне приходилось писать приложение для работы с веб-камерой на Delphi. Тогда я и столкнулся с этими функциями впервые. Посмотрев сорцы, я решил написать свою версию библиотеки и снабдить ее нужным функционалом. Взвод, готовность №1 Вполне возможно, что в одном компе/ноуте может быть несколько веб-камер. За примером далеко ходить не надо. Мне по работе часто приходится организовывать простенькие видеоконференции. Обычно в них участвуют два человека. Каждого из участников снимает отдельная камера. Сами камеры подключены к моему компу. Когда я начинаю съемку, то выбираю в программе для работы с видеокамерами нужную в настоящий момент камеру. Раз уж мы решили взять камеру под контроль, то обязаны разобраться, как получать список установленных устройств видеозахвата и выбрать то, с которым будем работать в настоящий момент. Для решения этой нехитрой задачи в WindowsAPI предусмотрена функция capGetDriverDescription(). Она принимает пять параметров: wDriverIndex – индекс драйвера видеозахвата. Значение индекса может варьироваться от 0 до 9; lpszName – указатель на буфер, содержащий соответствующее имя драйвера; cbName – размер (в байтах) буфера lpszName; lpszVer – указатель на буфер, содержащий описание определенного драйвера; cbVer – размер буфера (в байтах), в котором хранится описание драйвера. В случае успешного выполнения, функция вернет TRUE. Описание функции у нас есть, теперь посмотрим, как определить ее в C#. Делается это так: [DllImport("avicap32.dll")] protected static extern bool capGetDriverDescriptionA (short wDriverIndex, [MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszName, int cbName, [MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszVer, int cbVer); Обрати внимание, что перед тем, как указать имя подключаемой функции, в обязательном порядке требуется написать имя DLL, в которой она определена. В нашем случае это avicap32.dll. Так, функция импортирована, теперь можно написать класс, в котором она будет использоваться. Весь код класса для получения списка устройств я приводить не стану, покажу лишь код ключевого метода: public static Device[] GetAllCapturesDevices() { String dName = "".PadRight(100); String dVersion = "".PadRight(100); for (short i = 0; i < 10; i++) { if (capGetDriverDescriptionA(i, ref dName, 100, ref dVersion, 100)) { Device d = new Device(i); d.Name = dName.Trim(); d.Version = dVersion.Trim(); devices.Add(d); } } return (Device[])devices.ToArray (typeof(Device)); } Код выглядит проще некуда. Самое интересное место в нем – цикл, в котором происходит вызов упомянутой выше функции capGetDriverDescription. Из MSDN мы знаем, что индекс (первый параметр функции capGetDriverDescription()) может варьироваться от 0 до 9, поэтому мы целенаправленно запускаем цикл в этом диапазоне. Результатом выполнения метода будет массив классов Device (этот класс я определил самостоятельно, смотри соответствующие исходники). С получением списка устройств разобрались, теперь позаботимся об отображении видеопотока с камеры. Тут нам сослужит хорошую службу функция capCreateCaptureWindow(), предназначенная для создания окна захвата. Немного забегая вперед, скажу, что дальнейшие действия с камерой будут происходить путем банальной отправки сообщений окну захвата. Да, именно так, придется воспользоваться до боли знакомой windows-программисту (и приколисту) функцией SendMessage(). Теперь присмотримся внимательнее к функции capCreateCaptureWindow(). Ей требуется передать шесть аргументов: lpszWindowName – нуль-терминальная строка, содержащая имя окна захвата; dwStyle – стиль окна; x – координата X; y – координата Y; nWidth – ширина окна; nHeight – высота окна; hWnd – handle родительского окна; nID – идентификатор окна. Результатом выполнения функции будет handle созданного окна или NULL в случае ошибки. Поскольку эта функция также относится к WinAPI, то ее опять-таки нужно импортировать. Код импортирования приводить не буду, поскольку он практически идентичен тому, что я писал для функции capGetDriverDescription(). Лучше сразу взглянем на процесс инициализации камеры: deviceHandle = capCreateCaptureWindowA (ref deviceIndex, WS_VISIBLE | WS_CHILD, 0, 0, windowWidth, windowHeight, handle, 0); if (SendMessage(deviceHandle, WM_CAP_DRIVER_CONNECT, this.index, 0) > 0) { SendMessage(deviceHandle, WM_CAP_SET_SCALE, -1, 0); SendMessage(deviceHandle, WM_CAP_SET_PREVIEWRATE, 0x42, 0); SendMessage(deviceHandle, WM_CAP_SET_PREVIEW, -1, 0); SetWindowPos(deviceHandle, 1, 0, 0, windowWidth, windowHeight, 6); } В этом коде сразу после создания окна производится попытка отправки сообщения WM_CAP_DRIVER_CONNECT. Отличный от нуля результат выполнения функции расскажет нам о ее успешности. Теперь представим, что сегодня боги на нашей стороне и произведем незамедлительную отправку нескольких сообщений: WM_CAP_SET_SCALE, WM_CAP_SET_PREVIEWRATE, WM_CAP_SET_PREVIEW. Увы, как и в случае с функциями, C# ничего не знает о существовании этих констант. Тебе опять придется определять их самостоятельно. Список всех необходимых констант с комментариями я привел ниже. //Пользовательское сообщение private const int WM_CAP = 0x400; //Соединение с драйвером устройства видеозахвата private const int WM_CAP_DRIVER_CONNECT = 0x40a; //Разрыв связи с драйвером видеозахвата private const int WM_CAP_DRIVER_DISCONNECT = 0x40b; //Копирование кадра в буффер обмена private const int WM_CAP_EDIT_COPY = 0x41e; //Включение/отключение режима предпосмотра private const int WM_CAP_SET_PREVIEW = 0x432; //Включение/отключение режима оверлей private const int WM_CAP_SET_OVERLAY = 0x433; //Скорость previewrate private const int WM_CAP_SET_PREVIEWRATE = 0x434; //Включение/отключение масштабирования private const int WM_CAP_SET_SCALE = 0x435; private const int WS_CHILD = 0x40000000; private const int WS_VISIBLE = 0x10000000; //Установка callback-функции для preview private const int WM_CAP_SET_CALLBACK_FRAME = 0x405; //Получение одиночного фрейма с драйвера видеозахвата private const int WM_CAP_GRAB_FRAME = 0x43c; //Сохранение кадра с камеры в файл private const int WM_CAP_SAVEDIB = 0x419; Дальнейшее описание класса для работы с веб-камерой я опущу. Каркас я рассмотрел, а со всем остальным ты легко разберешься путем раскуривания моего хорошо прокомментированного исходника. Единственное, что я не хотел бы оставлять за кадром – это пример использования библиотеки. Всего в библиотеке я реализовал (точнее, дописал) пару методов: GetAllDevices (уже рассматривали), GetDevice (получение драйвера устройства видеозахвата по индексу), ShowWindow (отображение изображения с веб-камеры), GetFrame (захват отдельного кадра в графический файл) и GetCapture (захват видеопотока). В качестве демонстрации работоспособности изготовленной либы я набросал небольшое приложение. На форме я расположил один компонент ComboBox (используется для хранения списка имеющихся устройств видеозахвата) и несколько кнопок – "Обновить", "Пуск", "Остановить" и "Скриншот". Ах да, еще на моей форме пестреет компонент Image. Его я применяю для отображения видео с камеры. Разбор полетов начнем с кнопки "Обновить". По ее нажатию я получаю список всех установленных устройств видеозахвата. Начинка этого обработчика события: Device[] devices = DeviceManager.GetAllDevices(); foreach (Device d in devices) { cmbDevices.Items.Add(d); } Правда, все просто? Разработанная нами библиотека берет на себя все черную работу и нам остается лишь наслаждаться объектно-ориентированным программированием. Еще проще выглядит код для включения отображения видеопотока с камеры: Device selectedDevice = DeviceManager.GetDevice(cmbDevices.SelectedIndex); selectedDevice.ShowWindow(this.picCapture); Опять же, все проще пареной репы. Ну и теперь взглянем на код кнопки "Скриншот": Device selectedDevice = DeviceManager.GetDevice(cmbDevices.SelectedIndex); selectedDevice.FrameGrabber(); Я не стал уделять особого внимания методу FrameGrabber(). В моем исходнике вызов метода приводит к сохранению текущего кадра прямо в корень системного диска. Разумеется, это не очень корректно, поэтому перед боевым применением программы не забудь внести все необходимые поправки. Готовность № 3 Теперь настало время поговорить о том, как соорудить простенькую, но надежную систему видеонаблюдения. Обычно такие системы базируются на двух алгоритмах: различие двух фреймов и простое моделирование фона. Их реализация (код) достаточно объемна, поэтому в самый последний момент я решил пойти по более простому пути. Под легким путем подразумевается использование мощного, но пока малоизвестного фреймворка для .NET – AForge.NET. AForge.NET в первую очередь предназначен для разработчиков и исследователей. С его помощью, девелоперы могут существенно облегчить свой труд при разработке проектов для следующих областей: нейросети, работа с изображениями (наложение фильтров, редактирование изображений, попиксельная фильтрация, изменение размера, поворот изображения), генетика, робототехника, взаимодействие с видео устройствами и т.д. С фреймворком поставляется хорошая документация. В ней описаны абсолютно все возможности продукта. Не поленись хорошенько с ней ознакомиться. Особенно мне хочется отметить качество кода этого продукта. Все написано цивильно и копаться в коде – одно удовольствие. Теперь вернемся к нашей непосредственной задаче. Скажу честно, средствами фреймворка она решается как дважды два. "Тогда зачем ты мне парил мозг WinAPI функциями?" – недовольно спросишь ты. А за тем, чтобы ты не был ни в чем ограничен. Сам ведь знаешь, что проекты бывают разные. Где-то удобнее применить махину .NET, а где-то проще обойтись старым добрым WinAPI. Вернемся к нашей задачке. Для реализации детектора движений нам придется воспользоваться классом MotionDetector из вышеупомянутого фреймворка. Класс отлично оперирует объектами типа Bitmap и позволяет быстренько вычислить процент расхождения между двумя изображениями. В виде кода это будет выглядеть примерно так: MotionDetector detector = new MotionDetector( new TwoFramesDifferenceDetector( ), new MotionAreaHighlighting( ) ); //Обработка очередного кадра if ( detector != null ) { float motionLevel = detector.ProcessFrame( image ); if ( motionLevel > motionAlarmLevel ) { flash = (int) ( 2 * ( 1000 / alarmTimer.Interval ) ); } if ( detector.MotionProcessingAlgorithm is BlobCountingObjectsProcessing ) { BlobCountingObjectsProcessing countingDetector = (BlobCountingObjectsProcessing) detector.MotionProcessingAlgorithm; objectsCountLabel.Text = "Objects: " + countingDetector.ObjectsCount.ToString( ); } else { objectsCountLabel.Text = ""; } } } Вышеприведенный код (не считая инициализацию класса MotionDetector) у меня выполняется при получении очередного кадра с веб-камеры. Получив кадр, я выполняю банальное сравнение (метод ProcessFrame): если значение переменной motionlevel больше motionLevelAlarm (0.015f), то значит, надо бить тревогу! Движение обнаружено. На одном из скришотов хорошо видна работа демонстрация детектора движений. Готовность №4 Веб-камеру можно запросто приспособить для распознавания лиц и создания продвинутого способа лог-она в систему? Если переварив весь этот материал, ты думаешь, что это сложно, то ты ошибаешься! В конце марта на сайте http://codeplex.com (хостинг для OpenSource проектов от MS) появился пример (а затем и ссылка на статью), демонстрирующий реализацию программы для распознавания лиц с использованием веб-камеры. Сам пример основан на использовании новых возможностей .NET и SilverLight. Разобрать этот пример в рамках журнальной статьи нереально, так как автор исходника постарался и сделал все максимально шикарно. Тут тебе и алгоритмы для работы с изображениями (фильтр размытия, уменьшения шума, попиксельное сравнение, растяжка и т.д.) и демонстрация новинок SilverLight и много чего еще. Одним словом, must use! Ссылку на проект и статью ищи ниже. Конец фильма Приведенные в статье примеры послужат тебе хорошей отправной точкой. На их основе легко сварганить как профессиональную утилиту для работы с веб-камерой, и поднимать на ее продаже несколько сотен баксов в квартал или написать хитрого и злобного трояна-шпиона. Вспомни статью про бэкап Skype-бесед. В ней я говорил, что времена клавиатурных шпионов уже прошли. Сейчас особенно актуальны аудио и видеоданные. Если учесть, что сегодня веб-камера – обязательный атрибут любого ноутбука, то нетрудно представить, сколько интересного видео ты сможешь заснять, подсунув жертве "полезную программку"… Однако я тебе этого не говорил :). Удачи в программировании, а будут вопросы – пиши. WWW http://blogs.msdn.com/ – Русская версия статьи "Silverlight 4 real-time Face Detection" (распознавание лиц в реальном времени при помощи SilverLight). http://facelight.codeplex.com/ – здесь хостится проект "Facelight", позволяющий распознавать лица в реальном времени. Если ты собрался закодить серьезную софтину для определения лиц или логона в систему, то посмотреть на этот проект просто обязан. http://www.aforgenet.com/framework/ – тут ты найдешь AForge .NET – отличный и простой в использовании фреймворк для работы с видео, изображениями и т.д. http://vr-online.ru – все исходники примеров, а также кучу дополнительной информации ты можешь слить с сайта проекта VR-Online.