Фотографируем через WIA на .NET

в 21:50, , рубрики: .net, Canon, WIA, метки: , ,

imageЗадача поста — освежить информацию о работе с WIA в .NET, лежащую на просторах сети, т.к. большинство примеров безбожно устарели (они не работают), поделиться опытом с интересующимися и спросить совета у бывалых. Я думаю, совместно обсудить некоторые моменты, будет полезно для коллег, занимающихся подобными задачами. Постараюсь разжевать все моменты.

Мотив был таков: в программе, где хранится информация о людях есть их фотографии, но фотографии туда попадают сложным путем. Берем бумажный оригинал фото (их приносят люди вместе с документами), сканируем в файл, открываем в простом редакторе, кадрируем под 3х4см, сохраняем, в программе открываем анкету человека и там уже добавляем ему фото из готового файла.

Сейчас в программе несколько тысяч человек и стоит задача всех их сфотографировать красиво, потому что они приносят фотографии просто ужасного качества и с разным процентом заполнения лица. Дело за малым: повесить экран, выставить свет, стул, штатив, камера, USB… и наша программа.

У меня были большие планы по реализации этой функции:

  • приходит человек, причесывается, садится напротив камеры
  • оператор наводит камеру, поправляет человеку волосы, одежду… ну чтобы красиво
  • садиться за программу и нажимает кнопку «Сделать все автоматом»
  • все.

Тем временем программа должна:

  • подключиться к камере через WIA
  • заставить камеру сделать фотографию
  • получить из камеры свежий снимок
  • повернуть его в соответствии с данными о повороте
  • найти на фотографии лицо, определив прямоугольник, в который оно вписано
  • высчитать процент масштабирования так, чтобы прямоугольник лица занимал 80% кадра 3х4см при разрешении 300dpi
  • смасштабировать фото и наложить его на белый фон
  • сохранить фото в анкету человека

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

WIA

Windows Image Acquisition (WIA) — это простой способ работы с большинством моделей устройств обработки изображений (фотокамера, сканер, видеокамера и другие). Для успешной работы, нужно, чтобы устройство, как минимум, отобразилось в «Сканерах и камерах» Панели управления Windows. Чтобы это произошло — нужен подходящий драйвер. Какие-то устройства работают из коробки, а другим нужно устанавливать WIA-драйвер вручную, как в случае с фотокамерами Canon EOS.
Самая полезная статья MSDN, которая действительно помогает разораться с проблемами WIA — это статья с примерами. Я случайно наткнулся только на одну, must have.

Простейший код

Для простоты понимания, я приведу здесь код на VB.NET 2008 (имхо, когда объясняешь на пальцах — лучше так), и разберу его по полочкам. Сразу обращу внимание, что в нем опущены все субъективные методики, а также проверки на ошибки, чтобы максимально упростить пример. Продумывать логику приложения каждый должен сам, т.к. задачи у всех разные, и бывает очень сложно разобраться в чьем-то коде, особенно, когда тебе из него нужно лишь несколько строк.

Здесь исходный код формы Form1 с единственным элементом PictureBox1, растянутым на всю форму. И самое «сложное» здесь — добавить в параметрах проекта ссылку на COM-объект WIA. Перед запуском убедитесь, что ваше устройство включено.

Imports WIA

Public Class Form1

    Dim Device1 As WIA.Device
    Dim CommonDialog1 As New WIA.CommonDialogClass
    Dim DeviceManager1 As New WIA.DeviceManager

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' 1. Помогаем пользователю выбрать устройство
        Device1 = CommonDialog1.ShowSelectDevice(WiaDeviceType.CameraDeviceType)
        ' 2. Делаем снимок
        Device1.ExecuteCommand(WIA.CommandID.wiaCommandTakePicture)

        ' 3. Запоминаем какой объект нам нужен
        Dim newItem As WIA.Item = Device1.Items(Device1.Items.Count)
        ' 4. Подключаемся к устройству для получения фото
        Dim Device1a As WIA.Device = DeviceManager1.DeviceInfos(Device1.DeviceID).Connect
        ' 5. Читаем файл из устройства
        Dim newImage As WIA.ImageFile = CommonDialog1.ShowTransfer(newItem, WIA.FormatID.wiaFormatJPEG)
        ' 6. Преобразуем полученные данные в вектор 
        Dim newVector As WIA.Vector = newImage.FileData

        ' 7. Забираем из вектора байтовый массив, содержащий изображение
        Dim bytBLOBData() As Byte = newVector.BinaryData
        ' 8. Преобразуем массив в поток
        Dim stmBLOBData As New IO.MemoryStream(bytBLOBData)
        ' 9. Преобразуем поток в изображение и присваиваем его элементу PictureBox
        PictureBox1.Image = Image.FromStream(stmBLOBData)
        ' 10. Режим масштабирования Zoom помогает увидеть весь кадр (в целях отладки)
        PictureBox1.SizeMode = PictureBoxSizeMode.Zoom
    End Sub

End Class

После 7-го шага Вы вольны делать с изображением что угодно: сохранить в файл, сохранить в базу данных, вывести на форму, преобразовать, применить фильтры и т.д. Главное, что оно у вас есть.
Я, чтобы убедиться что все работает, отображал его на форме.

Вроде все просто

Как видите — код краток и насыщен полезными действиями. Но грабли и подводные камни — вечные спутники программистов по разные стороны Microsoft. Там думали, что все сделали просто и очевидно, а мы, как всегда, не поняли.

Шаг 1

        ' 1. Помогаем пользователю выбрать устройство
        Device1 = CommonDialog1.ShowSelectDevice(WiaDeviceType.CameraDeviceType)

Надо выбрать устройство. Есть два пути: выбрать самому или дать выбрать пользователю. Цель у обоих способов одна — получить ID устройства — DeviceID. Дальше можно работать с WIA, используя везде этот DeviceID как ссылку на нужное устройство.

Фотографируем через WIA на .NET

В примере открывается стандартное окно WIA для выбора устройства. Их может быть несколько.
Если нам не нужно, чтобы пользователь решал чем он хочет пользоваться, тогда можно отфильтровать доступные устройства, используя параметр WiaDeviceType. В примере я ищу фотокамеру — CameraDeviceType. Но берегитесь, некоторые картридеры прикидываются фотокамерами.
Здесь можно поступить так: пройтись циклом по всем устройствам и проверить, поддерживают ли они функцию wiaCommandTakePicture (сделать фотку). Соответственно, если таковое небыло найдено, но есть хотябы одно подходящее, то тут пользователю не отвертеться — придется ему решать, чем фотографировать. Как это сделать, хорошо описано в одном из примеров статьи MSDN (см.выше).

Камень №раз

Может статься, что ни одно устройство не поддерживает wiaCommandTakePicture. Это вводит в замешательство, особенно, если у вас Canon и стоят все программы с их диска, включая программу для Remote Shooting (удаленной съемки по USB). В ней он фотографирует, а через WIA не хочет. Здесь можно либо бодаться с Canon и клянчить у них SDK, которую они мало кому дают, а затем писать свой модуль, либо заставлять пользователя делать снимок вручную, а дальше все делать автоматом. Но в целом игра стоит свечь. Имейте в виду, что у Canon много подразделений, и SDK они распространяют каждое в своем регионе. Для России все грустно, но можно попробовать обратиться в Европейское отделение. Примерно так они мне ответили по почте.

Шаг 2

        ' 2. Делаем снимок
        Device1.ExecuteCommand(WIA.CommandID.wiaCommandTakePicture)

Ок, допустим нам повезло и можно фотографировать. Ур… а-э-э… What a f*ck?
Никаких настроек. Можно только сделать фото. Камера (читай — объектив) выезжает, щелкает в режиме полной автоматики (или что там на переключателе выставлено) и уезжает обратно. Ну, и на том спасибо.
Честно говоря, я не углублялся в проблематику предварительной настройки камеры удаленно, но потуги у населения замечены при помощи Гугла. Возможно кто-то смог найти решение.
Камера делает кадр и сохраняет его в свою память или карту. Теперь его надо оттуда достать!

Шаг 3

        ' 3. Запоминаем какой объект нам нужен
        Dim newItem As WIA.Item = Device1.Items(Device1.Items.Count)

Пока писал статью, похоже, понял причину проблемы с Камнем №два. Спасибо Харб!
На этом шаге, мы запоминаем ID последнего файла, хранящегося в камере, т.к. объект Device1.Items хранит информацию обо всех файлах и папках, хранящихся в памяти камеры. Да да, и папок тоже — будьте внимательны. Через этот объект можно получить любую информацию, например имя файла. Это все есть в примерах MSDN (см.выше).

Шаг 4

        ' 4. Подключаемся к устройству для получения фото
        Dim Device1a As WIA.Device = DeviceManager1.DeviceInfos(Device1.DeviceID).Connect

Еще раз подключаемся к камере, но уже в другом режиме. Здесь мы подключаемся к ней, как вместилищу файлов. Будьте бдительны! Структура файлов запоминается на момент подключения! Об этом дальше...

Шаг 5

        ' 5. Читаем файл из устройства
        Dim newImage As WIA.ImageFile = CommonDialog1.ShowTransfer(newItem, WIA.FormatID.wiaFormatJPEG)

И хватаем файл, указатель на который запомнили.
Здесь процесс загрузки фотографии будет показан стандартным WIA-интерфейсом с прогрессбаром.
Одним из параметров функции является формат передаваемого файла. Мне вот нужен wiaFormatJPEG. Это чтобы фотоаппарат вдруг не отдал мне TIFF. (шутка)

Камень №два

Если передрать мой пример 1 в 1, то возникает одна маленькая проблема. Форма будет всегда показывать предпоследнюю фотографию. Почему? Возможно проблема именно в порядке шагов 4 и 5. А грабли здесь заключаются в том, что функция, выдающая последнюю сделанную фотографию, запоминает структуру файлов камеры на момент подключения. Так что на 5-ом шаге я получаю предпоследнюю фотографию. Вот почему используется новое подключение — нужно обновить структура файлов после фотографирования.

Пытливый ум не мог не проверить эту теорию и тут же доказал ее. Поменяв местами Шаг 4 и 5 — получим именно новую фотографию. Только теперь нужно обращаться к устройству по новому имени.

        ' 5. Подключаемся к устройству для получения фото
        Dim Device1a As WIA.Device = DeviceManager1.DeviceInfos(Device1.DeviceID).Connect
        ' 4. Запоминаем какой объект нам нужен
        Dim newItem As WIA.Item = Device1a.Items(Device1a.Items.Count)
Шаг 6

        ' 6. Преобразуем полученные данные в вектор 
        Dim newVector As WIA.Vector = newImage.FileData

Единственный документированный способ получения фотографии — это ее получение через вектор-интерфейс. В итоге, в этом объекте будет много всего, но нас интересует лишь один — массив байт BinaryData, содержащий изображение.

Шаги 7,8,9

        ' 7. Забираем из вектора байтовый массив, содержащий изображение
        Dim bytBLOBData() As Byte = newVector.BinaryData
        ' 8. Преобразуем массив в поток
        Dim stmBLOBData As New IO.MemoryStream(bytBLOBData)
        ' 9. Преобразуем поток в изображение и присваиваем его элементу PictureBox
        PictureBox1.Image = Image.FromStream(stmBLOBData)

Этот BinaryData требует парочки преобразований, прежде чем он станет полноценной картинкой. Байты в поток, поток в изображение. А дальше можно делать с полученным объектом что угодно (согласно комментариям в листинге).
К слову: указанным методом изображения преобразуются для хранения в СУБД (только преобразование идет в обратную сторону).

Шаг 10

        PictureBox1.SizeMode = PictureBoxSizeMode.Zoom

Каждому свой путь, ну а я для наглядности, присвоил картинку свойству Image элемента PictureBox1. Чтобы посмотреть на себя тепленького.

Теперь осталось только обеспечить обработку ошибок, т.к. WIA — очень капризная и ситуаций будет много (больше всего их будет при разработке под x64-систему, потому что во время отладки в случае ошибки, код не выдаст исключения, а просто не будет выполнен начиная с места возникновения ошибки). Камера, например, может уснуть, пока ей долго не будут пользоваться. Многое нужно предусмотреть.

Очень часто применяется эта технология для работы типа «бюро пропусков» с фотографиями.

Теперь о вопросах для обсуждения:

  1. У кого-нибудь есть SDK для Canon? Можете ли поделиться необходимыми DLL-ками для некоммерческого использования?
  2. Чем посоветуете в .NET распознать прямоугольник, в который вписано лицо? Возможно даже с положением глаз, чтобы центрировать лицо на фото. (обещаю выложить здесь конечный результат)

Спасибо за внимание!

Автор: NikitaTratorov

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js