Некоторые люди опасаются, что встроенная в их ноутбуки веб-камера может за ними подглядывать. Они опасаются этого настолько серьезно, что порой даже заклеивают изолентой ее недремлющее око. Делают они это не зря. Мы расскажем, как можно программно овладеть встроенной в ноутбук веб-камерой и использовать ее функционал как в мирных, так и не очень целях.
Статья опубликована в журнале «Хакер» №138, июль 2010.

Начинаем реализацию: первые досадные огорчения
Я был очень удивлен и расстроен, когда узнал, что в великом и могучем .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));
}

WinAPI в королевстве .NET
Код выглядит проще некуда. Самое интересное место в нем — цикл, в котором происходит вызов упомянутой выше функции capGetDriverDescription. Из MSDN мы знаем, что индекс (первый параметр функции capGetDriverDescription()) может варьироваться от 0 до 9, поэтому мы целенаправленно запускаем цикл в этом диапазоне. Результатом выполнения метода будет массив классов Device (этот класс я определил самостоятельно, смотри соответствующие исходники).
С получением списка устройств разобрались, теперь позаботимся об отображении видеопотока с камеры. Тут нам сослужит хорошую службу функция capCreateCaptureWindow(), предназначенная для создания окна захвата.
Немного забегая вперед, скажу, что дальнейшие действия с камерой будут происходить путем банальной отправки сообщений окну захвата. Да, именно так, придется воспользоваться до боли знакомой windows-программисту (и приколисту) функцией SendMessage().
Теперь присмотримся внимательнее к функции capCreateCaptureWindow(). Ей требуется передать шесть аргументов:
- lpszWindowName — нуль-терминальная строка, содержащая имя окна захвата;
- dwStyle — стиль окна;
- x — координата X;
- y — координата Y;
- nWidth — ширина окна;
- nHeight — высота окна;
- hWnd — handle родительского окна;
- nID — идентификатор окна.
Результатом выполнения функции будет handle созданного окна или NULL в случае ошибки.
HTTP://WWW — links
- blogs.msdn.com — Русская версия статьи «Silverlight 4 real-time Face Detection» (распознавание лиц в реальном времени при помощи SilverLight).
- facelight.codeplex.com — здесь хостится проект «Facelight», позволяющий распознавать лица в реальном времени. Если ты собрался закодить серьезную софтину для определения лиц или логона в систему, то посмотреть на этот проект просто обязан.
- www.aforgenet.com/framework — тут ты найдешь AForge.NET — отличный и простой в использовании фреймворк для работы с видео, изображениями и т.д.
- vr-online.ru — все исходники примеров, а также кучу дополнительной информации ты можешь слить с сайта проекта VR-Online.
Поскольку эта функция также относится к 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 и много чего еще. Ссылку на проект и статью ищи во врезке.
Конец фильма
Приведенные в статье примеры послужат тебе хорошей отправной точкой. На их основе легко сварганить профессиональную утилиту для работы с веб-камерой и поднимать на ее продаже несколько сотен баксов в квартал, или написать хитрого и злобного трояна-шпиона. Вспомни статью про бэкап Skype-бесед. В ней я говорил, что времена клавиатурных шпионов уже прошли. Сейчас особенно актуальны аудио и видеоданные. Если учесть, что сегодня веб-камера — обязательный атрибут любого ноутбука, то нетрудно представить, сколько интересного видео ты сможешь заснять, подсунув жертве «полезную программку»… Однако я тебе этого не говорил :). Будут вопросы — пиши.