- PVSM.RU - https://www.pvsm.ru -

Распознаём изображение с токена при помощью камеры

В организации, где я тружусь в свободное от отдыха время, очень высокие требования к безопасности. Везде, где только можно, для аутентификации пользователей используются токены [1]. Мне выдали вот такую вот штуку:
Распознаём изображение с токена при помощью камеры
и сказали: жмёшь кнопку, смотришь цифры, вводишь пароль и радуешься. «Безопасность, конечно, превыше всего, но и о комфорте забывать не следует» — примерно так подумал я и провёл ревизию имеющегося у меня электронного хлама.

(Честно говоря, мне давно хотелось отвлечься от программирования и поковыряться в железяках, поэтому моя лень — не единственный мотиватор во всей этой затее).

Анализ технического задания и подбор компонентов

Первым делом на глаза мне попалась старая вем-камера «Logitech QuickCam 3000», отправившаяся в «хлам» в связи с покупкой ноутбука со встроенной веб-камерой. План созрел моментально: снимаем камерой циферки с токена, распознаём их на компьютере и… Профит! Дальше был найден сервопривод (ну не нажимать же кнопку на токене каждый раз руками, правда?), старый USB-хаб (в моём ноутбуке всего два USB-порта, и один из них постоянно занят USB-Ethernet адаптером), и плата Arduino, неизвестно каким образом у меня оказавшаяся. В качестве корпуса для своего девайса я решил использовать конструктор «Лего», купленный впрок моему годовалому сыну (кажется, теперь я понимаю, почему жена ехидно ухмыльнулась при покупке).

К сожалению, фотографий донорских девайсов в целости и сохранности у меня нет, поэтому я могу «похвастаться» только устройством в сборе:

Распознаём изображение с токена при помощью камеры

Принципиальная схема и прошивка микроконтроллера

Собственно, всё до смешного просто (я даже схему рисовать не буду): USB-хаб, к которому подключена веб-камера и ардуина. К ардуине подключен сервопривод (через ШИМ). Вот и всё.Исходный код программы, которая заливается в ардуину, тривиален: github.com/dreadatour/lazy/blob/master/servo.ino [2]
Ардуина ждёт букву 'G' на ком-порт, и при её поступлении дёргает серву туда-назад. Задержка (500мс) и угол наклона сервы были подобраны экспериментальным путём.

Выбор языка программирования и анализ существующих библиотек «компьютерного зрения»

Единственным языком программирования, которому бы я доверил такую сложную задачу, как «компьютерное зрение» — благославленный Python. Благо в нём, практически из коробки, есть биндинги к такой славной библиотеке, как OpenCV [3]. Собственно, на этом и остановимся.

Алгоритм распознавания кода токена

Я буду давать ссылки на куски кода, которые отвечают за описываемый функционал — на мой взгляд это оптимальный формат подачи информации. Весь код можно посмотреть на гитхабе [4].

Для начала берём изображение с камеры [5]:
Распознаём изображение с токена при помощью камеры

Облегчаем себе задачу: камера неподвижна относительно основы, токен может двигаться в своём лотке совсем незначительно, поэтому находим границы возможных положений токена, обрезаем кадр [6] с камеры и (исключительно для нашего удобства) поворачиваем изображение [7] на 90˚:
Распознаём изображение с токена при помощью камеры

Дальше делаем некоторые преобразования [8]: конвертируем получившееся изображение в grayscale и находим границы с помощью детектора границ Канни [9] — это будут границы ЖК-экрана токена:
Распознаём изображение с токена при помощью камеры

Находим контуры [10] на полученном изображении. Контур представляет собой массив линий — отбрасываем линии меньше определённой длины:
Распознаём изображение с токена при помощью камеры

С помощью тупого алгоритма определяем четыре линии [11], ограничивающие наш ЖК-дисплей:
Распознаём изображение с токена при помощью камеры

Находим точки персечения [12] этих линий и выполняем несколько проверок:

  • проверяем [13], чтобы длина вертикальных и горизонтальных линий примерно совпадала друг с другом и чтобы длина линий примерно совпадала с размером ЖК-дисплея (который мы вычислили экспериментальным путём)
  • проверяем [14], чтобы диагонали были примерно равны (нам нужно прямоугольник — ЖК-дисплей)

Далее [15] находим угол, на который повёрнут наш токен, поворачиваем изображение на этот угол и вырезаем изображение ЖК:
Распознаём изображение с токена при помощью камеры

Самое сложное позади. Теперь нам нужно повысить контрастность полученного изображения. Для этого мы запоминаем [16] изображение пустого ЖК-экрана (до нажатия на кнопку токена), и просто «вычитаем [17]» это изображение из картинки с цифрами (после нажатия [18] на кнопку):
Распознаём изображение с токена при помощью камеры

Получаем чёрно-белое изображение. Для этого с помощью ещё одного тупого алгоритма [19] находим оптимальный порог, который будет разделять все пиксели на изображении на «чёрные» и «белые», конвертируем изображение в Ч/Б и вырезаем [20] символы:
Распознаём изображение с токена при помощью камеры

Ну а дальше просто распознаём цифры. Нам нет смысла мучаться с нейронными сетями и прочими штуками, т.к. у нас семисегментный индикатор: есть семь «точек», по которым однозначно определяется [21] каждая цифра:
Распознаём изображение с токена при помощью камеры

На всякий случай распознаём цифры с нескольких кадров: если три кадра подряд мы получили одинаковый результат — считаем, что распознавание прошло успешно, выводим результат с помощью программы «growlnotify» пользователю и копируем полученный код в буфер обмена.

Видео работы девайса

Осторожно, звук!

Исходный код целиком [22]

Автор: Dreadatour


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/6616

Ссылки в тексте:

[1] токены: http://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BA%D0%B5%D0%BD_(%D0%B0%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8)

[2] github.com/dreadatour/lazy/blob/master/servo.ino: https://github.com/dreadatour/lazy/blob/master/servo.ino

[3] OpenCV: http://opencv.willowgarage.com/

[4] на гитхабе: https://github.com/dreadatour/lazy/blob/master/lazy.py

[5] берём изображение с камеры: https://github.com/dreadatour/lazy/blob/master/lazy.py#L42

[6] обрезаем кадр: https://github.com/dreadatour/lazy/blob/master/lazy.py#L44-48

[7] поворачиваем изображение: https://github.com/dreadatour/lazy/blob/master/lazy.py#L50-53

[8] делаем некоторые преобразования: https://github.com/dreadatour/lazy/blob/master/lazy.py#L127-133

[9] детектора границ Канни: http://en.wikipedia.org/wiki/Canny_edge_detector

[10] Находим контуры: https://github.com/dreadatour/lazy/blob/master/lazy.py#L135-138

[11] определяем четыре линии: https://github.com/dreadatour/lazy/blob/master/lazy.py#L142-158

[12] точки персечения: https://github.com/dreadatour/lazy/blob/master/lazy.py#L163-167

[13] проверяем: https://github.com/dreadatour/lazy/blob/master/lazy.py#L177-187

[14] проверяем: https://github.com/dreadatour/lazy/blob/master/lazy.py#L189-191

[15] Далее: https://github.com/dreadatour/lazy/blob/master/lazy.py#L193-196

[16] запоминаем: https://github.com/dreadatour/lazy/blob/master/lazy.py#L259

[17] вычитаем: https://github.com/dreadatour/lazy/blob/master/lazy.py#L275

[18] нажатия: https://github.com/dreadatour/lazy/blob/master/lazy.py#L263

[19] тупого алгоритма: https://github.com/dreadatour/lazy/blob/master/lazy.py#L277-282

[20] вырезаем: https://github.com/dreadatour/lazy/blob/master/lazy.py#L286-290

[21] определяется: https://github.com/dreadatour/lazy/blob/master/lazy.py#L291

[22] Исходный код целиком: https://github.com/dreadatour/lazy