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

Статья опубликована в журнале «Хакер» №138, июль 2010.

Подглядываем через веб-камеру

Начинаем реализацию: первые досадные огорчения

Я был очень удивлен и расстроен, когда узнал, что в великом и могучем .NET Framework напрочь отсутствует возможность простого взаимодействия с веб-камерами. В четвертой версии ситуация улучшилась (для SilverLight-проектов точно появились соответствующие классы), но протестировать я их не успел, поскольку пример для данной статьи я начал писать еще до официального выхода VS2010 и 4-го .NET’a.

Практически отчаявшись, я плотно засел в гугле. Результаты поиска по рунету меня не вдохновили — все, что я нашел — это ссылки на MSDN и технологию DirectDraw. Я даже попробовал набросать простенький примерчик, но из-за отсутствия опыта работы с DirectDraw меня постиг облом. У меня получилось собрать совсем простенькое приложение, но я так и не смог выловить в нем все глюки. Еще больше отчаявшись, я принялся шерстить ресурсы наших западных товарищей. Проштудировав несколько десятков ссылок, я смог нарыть много вкусностей. Среди них были всевозможные примеры и небольшие статейки (американцы не любят много писать). Мне даже удалось найти рабочий пример на основе DirectDraw, но когда я увидел код — ужаснулся. Разобраться в нем было тяжело. Поэтому я решил с ним не заморачиваться, а попытаться найти способ попроще. Не успел я распрощаться с примером на DirectDraw, как на глаза мне попался еще один. Автор примера закодил целую библиотеку для работы с веб-камерами и другими устройствами видеозахвата, используя технологию VFW (Video For Windows). Жаль, что проект автора (я про библиотеку) был максимально кастрирован. Все, что позволяла сделать библиотека — вывести изображение с веб-камеры. Ни захвата отдельных кадров, ни записи видео и других полезных нам фич не было. И тем не менее, мое подсознание решительно сказало мне, что этот проект и есть то, что я искал. Не успел я беглым взглядом пробежаться по его коду, как увидел имена знакомых win-сообщений и не менее знакомых названий WinAPI функций. Когда-то давным-давно мне приходилось писать приложение для работы с веб-камерой на Delphi. Тогда я и столкнулся с этими функциями впервые. Посмотрев сорцы, я решил написать свою версию библиотеки и снабдить ее нужным функционалом.

Взвод, готовность №1

Вполне возможно, что в одном компе/ноуте может быть несколько веб-камер. За примером далеко ходить не надо. Мне по работе часто приходится организовывать простенькие видеоконференции. Обычно в них участвуют два человека. Каждого из участников снимает отдельная камера. Сами камеры подключены к моему компу. Когда я начинаю съемку, то выбираю в программе для работы с видеокамерами нужную в настоящий момент камеру. Раз уж мы решили взять камеру под контроль, то обязаны разобраться, как получать список установленных устройств видеозахвата и выбрать то, с которым будем работать в настоящий момент.

Для решения этой нехитрой задачи в WindowsAPI предусмотрена функция capGetDriverDescription(). Она принимает пять параметров:

  1. wDriverIndex — индекс драйвера видеозахвата. Значение индекса может варьироваться от 0 до 9;
  2. lpszName — указатель на буфер, содержащий соответствующее имя драйвера;
  3. cbName — размер (в байтах) буфера lpszName;
  4. lpszVer — указатель на буфер, содержащий описание определенного драйвера;
  5. 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

WinAPI в королевстве .NET

Код выглядит проще некуда. Самое интересное место в нем — цикл, в котором происходит вызов упомянутой выше функции capGetDriverDescription. Из MSDN мы знаем, что индекс (первый параметр функции capGetDriverDescription()) может варьироваться от 0 до 9, поэтому мы целенаправленно запускаем цикл в этом диапазоне. Результатом выполнения метода будет массив классов Device (этот класс я определил самостоятельно, смотри соответствующие исходники).

С получением списка устройств разобрались, теперь позаботимся об отображении видеопотока с камеры. Тут нам сослужит хорошую службу функция capCreateCaptureWindow(), предназначенная для создания окна захвата.

Немного забегая вперед, скажу, что дальнейшие действия с камерой будут происходить путем банальной отправки сообщений окну захвата. Да, именно так, придется воспользоваться до боли знакомой windows-программисту (и приколисту) функцией SendMessage().

Теперь присмотримся внимательнее к функции capCreateCaptureWindow(). Ей требуется передать шесть аргументов:

  1. lpszWindowName — нуль-терминальная строка, содержащая имя окна захвата;
  2. dwStyle — стиль окна;
  3. x — координата X;
  4. y — координата Y;
  5. nWidth — ширина окна;
  6. nHeight — высота окна;
  7. hWnd — handle родительского окна;
  8. 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-бесед. В ней я говорил, что времена клавиатурных шпионов уже прошли. Сейчас особенно актуальны аудио и видеоданные. Если учесть, что сегодня веб-камера — обязательный атрибут любого ноутбука, то нетрудно представить, сколько интересного видео ты сможешь заснять, подсунув жертве «полезную программку»… Однако я тебе этого не говорил :). Будут вопросы — пиши.

Сканы статьи

Страница журнала, часть 1 Страница журнала, часть 2 Страница журнала, часть 3 Страница журнала, часть 4
Страница журнала, часть 1
×
Страница журнала, часть 2
×
Страница журнала, часть 3
×
Страница журнала, часть 4
×