Деякі люди побоюються, що вбудована в їх ноутбуки вебкамера може за ними підглядати. Вони побоюються цього настільки серйозно, що часом навіть заклеюють ізоляційною стрічкою її недремне око. Роблять вони це не дарма. Ми розповімо, як можна програмно опанувати вбудованої в ноутбук веб-камерою і використовувати її функціонал як в мирних, так і не дуже цілях.
Починаємо реалізацію: перші прикрі засмучення
Я був дуже здивований і засмучений, коли дізнався, що у великому і могутньому. 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));
}
Код виглядає простіше нікуди. Найцікавіше місце в ньому - цикл, у якому відбувається виклик згаданої вище функції 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 в разі помилки. Оскільки ця функція також належить до 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-бесід. У ній я говорив, що часи клавіатурних шпигунів вже пройшли. Зараз особливо актуальні аудіо і відео. Якщо врахувати, що сьогодні веб-камера - обов'язковий атрибут будь-якого ноутбука, то неважко уявити, скільки цікавого відео ти зможеш зняти, підсунувши жертві "корисну програмку" ... Однак я тобі цього не говорив:). Удачи в програмуванні, а будуть питання - пиши.