Русский
Русский
English
Статистика
Реклама

Обработка изображений

Перевод Генерация изображений с помощью echoprintf в 5 строчках кода без библиотек и заголовков

25.04.2021 12:10:20 | Автор: admin
tl;dr: форматы файлов Netpbm позволяют легко выводить пиксели, используя только текстовый ввод-вывод.



Вот весь генерирующий это изображение скрипт bash без зависимостей:

#!/bin/bashexec > my_image.ppm    # Все инструкции echo будут писать в этот файлecho "P3 250 250 255"  # формат, ширина, высота, максимальное значение цветаfor ((y=0; y<250; y++)) {  for ((x=0; x<250; x++)) {    echo "$((x^y)) $((x^y)) $((x|y))" # r, g, b  }}

Это все, что нужно для генерации изображения, которое можно будет считать стандартными инструментами вроде GIMP, ImageMagick и Netpbm.

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

Тем не менее если изначально не знать о примере выше, то порог для реализации вывода графики может показаться достаточно высоким. Даже если использовать библиотеку single_file, это будет означать необходимость настройки еще одного компонента. И это особенно неприятно в случае отладки, когда знаешь, что буквально в течение часа его удалишь.

К счастью, пакет инструментов Netpbm предлагает на удивление гибкое решение: набор форматов файлов с наименьшим общим знаменателем для полноцветных Portable PixMaps (PPM), Portable GreyMaps (PGM) и монохромных Portable BitMaps (PBM), которые все можно записать в виде простого текста ASCII через базовый ввод-вывод любого языка.

Все вместе эти форматы известны как PNM: Portable aNyMaps.

Вышеприведенного скрипта bash вполне достаточно для начала, тем не менее подробное описание этого формата файлов можно найти в man ppm, man pgm и man pbm в системе, где установлен Netpbm.

Каждая страница руководства описывает две версии простого формата: двоичную и в кодировке ASCII. Любая из них реализуется очень легко, хотя ASCII-вариант мне нравится больше за его полнейшую простоту, которая позволяет прописывать такие файлы даже в Notepad.

Для преобразования их в более распространенные форматы нужно либо выполнить экспорт в GIMP, либо использовать команду ImageMagick convert my_file.ppm my_file.png, либо команду NetPBM pnmtopng < my_file.ppm > my_file.png.

Если вы решите передать изображения, используя этот простой формат ASCII, то команда NetPBM pnmtoplainpnm преобразует двоичный ppm/pgm/pbm (создаваемый любым инструментом, включая anytopnm из Netpbm) в ASCII ppm/pgm/pbm.

Если вы захотите поэкспериментировать с каким-либо алгоритмом обработки изображений, то можете легко задействовать прекрасный набор инструментов Netpbm путем чтения/записи PPM через stdin/stdout:

curl http://example.com/input.png |     pngtopnm |     ppmbrighten -v +10 |    yourtoolhere |    pnmscale 2 |    pnmtopng > output.png

Подробнее..

Балет и роботы

04.06.2021 12:23:32 | Автор: admin
Продолжаю рассказывать о своём необычном увлечении. Моё хобби заключается в алгоритмическом преобразовании древнего черно-белого видео в материал, который выглядит современно. Про мою первую работу написано в этой статье. Прошло время, мои навыки улучшились, и теперь я не смеюсь над мемом Zoom and enhance.


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

Необходимо уточнение, что речь пойдёт не про ручную реставрацию и оцветнение, которые требуют тонны индусов и килограммов денег, а про использование автонастраивающихся алгоритмов (часто именуемых терминами ИИ, нейросети).
Когда-то мои работы были незначительно лучше по четкости и цвету аналогичных работ других любителей, теперь же арсенал применяемых средств расширился настолько, что качество конечного результата зависит лишь от вложенного времени.

Небольшая история


Со стороны, процесс автоматического оцветнения видео не воспринимается как нечто заумное, ведь очевидно же, что достаточно скачать некую программку и бросить в неё видеозаписью, а всю сложную работу уже проделал тот, кто сконструировал алгоритм оцветнения и потратил электричество на обучение.

А давайте я вам сейчас расскажу про то, как появился революционный алгоритм оцветнения Deoldify. Даже если вы занимаетесь машинным обучением, не факт, что вы знаете кто такой Джереми Говард (Jеremy Howard). Его профессиональная карьера началась с роли наёмного консультанта, 20 лет назад он занимался тем, что сейчас именуется Data Science, то есть извлечение прибыли из данных с помощью математики.

Продажа парочки стартапов, позволила ему задуматься над осознанным внесением позитивного вклада в развитие человечества. После переезда в Dолину он влился в тусовку топовых специалистов по машинному обучению, а в 2011 он стал лучшим участником соревнований Kaggle.

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

image

У Джереми сформировалось четкое понимание того, что настолько мощный инструмент может стать источником роста в любой области. Главная проблема была (и есть) в том, что количество специалистов по самообучающимся системам несопоставимо с количеством проектов, в которых можно было бы задействовать их способности. С его точки зрения гораздо эффективнее было бы дать этот инструмент всем желающим. Так появился проект Fast.Ai, который является симбиозом кода и курса обучения. Код с одной стороны значительно упрощает использование Pytorch (средство для конструирования алгоритмов машинного обучения), а с другой содержит массу готовых приёмов, которыми пользуются профессионалы для повышения скорости и качества обучения. Учебный курс строится по принципу сверху вниз, сначала студенты обучаются использовать готовые конвейеры, затем Джереми показывает как можно написать каждый элемент конвейера с нуля, начиная с живой демонстрации в табличке Excel ключевого алгоритма, лежащего в основе всего Deep Learning. Цель проекта Fast.Ai состоит в том, чтобы научить специалиста из любой области решать типичные задачи на типичных архитектурах (конечно, при наличии способностей к программированию). Чудес не бывает, уровень навыков после такого обучения не превосходит уровень круглое кати квадратное верти, но даже этого достаточно для решения рабочих задач на новом уровне, недоступном коллегам.

В обучающем курсе Fast.Ai одна из тем посвящена использованию архитектуры UNet, которая ориентирована на переинтерпретацию изображений. Например, эту архитектуру можно обучить генерировать реалистичные фотографии из изображений, полученных с тепловизора, или контрастно выделять аномалии на снимках. Если говорить обобщенно, то такая архитектура по известной форме и свойствам, позволяет предсказать наличие у формы свойств, выявление которых было целью обучения.
image
В качестве домашней работы студентам курса предлагалось использовать UNet для решения любой интересной задачи. Некий Jason Antic заинтересовался преобразованием черно-белых фотографий в цветные. Его эксперименты показали, что такая архитектура производит адекватные результаты и есть значительный потенциал для дальнейшего развития. Так появился проект Deoldify, который при содействии самого Джереми Говарда вырос до законченного продукта и в конце концов взорвал интернет. Автор сделал первую версию доступной всем желающим, а сам занялся развитием закрытой коммерческой версии, которую в ограниченном виде можно использовать на генеалогическом проекте MyHeritage.com (требует регистрации, несколько фотографий бесплатно).

image


Кружок Умелые ручки


Главная проблема использования передовых проектов с открытым кодом в области машинного обучения заключается в том, что дружелюбность к пользователю обычно левее нуля. Автор проекта сосредоточен на конвейере обучения, результаты работы алгоритма ему нужны исключительно для презентации сообществу, что нормально, поскольку цель таких проектов это самопиар и вклад в исследования. Самостоятельный допил проектов пользователем является нормой. Чтобы далеко не ходить: перед обработкой видео его необходимо декодировать, обработать каждый кадр и получившееся сжать в видеофайл, если одно видео обрабатывать несколькими инструментами, то после последовательных пережатий про качество можно забыть. Каждый новый инструмент приходится переделывать на работу с пачкой картинок. А что если в инструменте на уровне конвейера вшито использование за один запуск не более 8 кадров? Для демонстрации алгоритма достаточно, для практических целей нет. Придётся писать внешнюю обертку для многократного запуска, ведь маловероятно, что получится изменить чужой конвейер без потери совместимости с предобученным состоянием алгоритма. И, конечно, академические авторы не особо парятся на тему оптимизации. Есть один проект, который отказывался работать с изображениями больше чем спичечный коробок, после оптимизации он стал требовать в 5 раз меньше видеопамяти и теперь переваривает FullHd.

Можно долго перечислять косяки, которые встречались, достаточно остановиться на том факте, что для работы любого алгоритма необходима установка инструментальных библиотек, иногда может потребоваться 2-3 дня экспериментов прежде чем библиотеки перестанут конфликтовать между собой (даже при наличии перечня точных версий существует масса причин почему придётся долго гуглить).

Минутка прекрасного


Выбирать материал для оцветнения не то чтобы очень просто. С одной стороны содержание должно быть любопытно мне, с другой стороны длинный рекламный фильм фирмы Дизеля, насыщенный техническими деталями, вряд ли заинтересует широкую аудиторию, с третьей стороны существуют ограничения в выборе из-за авторских прав. Новые варианты приходят из памяти или в ходе розыска конкретных записей. Мои последние работы посвящены русской балерине Анне Павловой. Про неё достаточно написано и сказано, сохранилось множество фотографий, но так как её профессиональная деятельность связана с движением во времени и пространстве, то самым интересным свидетелем является киноплёнка. К сожалению, часть сохранившихся записей неизвестны широкой публике, а то что сейчас находится поиском совершенно отвратительного качества. Чем интересна фигура Анна Павловой, так это в буквальном смысле фигурой. Её можно считать прообразом эталона современной балерины, возможно, для вас не будет открытием, что, в конце 19-го века, худоба всё ещё коллективно воспринималась как признак болезни либо бедности, конечно, в среде обеспеченных людей встречались разные фигуры, но в целом, упитанность воспринималась, как маркер успешной жизни. Пышущие здоровьем женщины часто выступали на театральной сцене, вот фотографии трёх звёзд того времени.
image

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

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

Проблемы на старте


Декодируем исходное видео в серию файлов PNG. Просматриваем полученные изображения и замечаем, что встречаются кадры, которые повторяют предыдущие.

image

Это стандартная история, ведь на заре кинематографа для экономии пленки использовалась скорость съёмки 12-19 кадров в секунду (далее fps). В более позднюю аналоговую эпоху, когда 99% видеоряда имело скорость 24-25 fps, для демонстрации старых лент использовалось копирование кадр-в-кадр, что приводило к ускоренному воспроизведению. Поэтому в сознании большинства старая хроника прочно ассоциируется с нечеткими мечущимися человечками. Правда заключается в том, что черно-белые оригиналы плёнок очень хорошо сохраняются, даже лучше чем цветные, и имеют разрешение между DVD и FullHd. Всё что вы могли видеть в большинстве случаев было дрянными копиями, переснятыми с проекции на экран. Хотя многие киноленты сохранились лишь на таких копиях (утраты происходят из-за человеческого фактора), всё же количество сохранившихся оригиналов значительно. Доступ к оригиналам имеют только избранные, к счастью, в наши дни компьютерная обработка изображений позволяет неограниченно тиражировать сканированные качественные копии оригиналов, почистить дефекты и воспроизводить материал с нормальной скоростью частоты кадров.

С низкой частотой кадров есть две раздельные проблемы. Во-первых, она нестандартная, если на личном компьютере можно использовать любую скорость воспроизведения, то существует масса случаев, когда необходимо придерживаться диапазона 24-30 fps. Самый простой способ коррекции частоты кадров это через каждые 3-4 кадра повторять последний. Скорость движения объектов при этом становится естественной, но картинка воспринимается как дёрганная, это собственно вторая проблема. В 2021 году технологии позволяют сделать плавную картинку за счет интерполяции кадров. Технология интерполяции кадров в телевизорах и программных видеопроигрывателях начала встречаться примерно с 2005 г. За счет математических алгоритмов два соседних изображения смешиваются так, что при проигрывании возникает ощущение плавного движения в кадре. Это неплохо получается для 24 fps, так как разница между кадрами редко бывает значительной. Но для 12-19 fps такие алгоритмы не годятся: они рисуют смазанное двойное изображение или сумасшедшие артефакты. Эту задачу успешнее решают самообучающиеся алгоритмы, которые способны запоминать как именно следует рисовать промежуточное изображение для разного движения разных типов объектов.

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

Неожиданный поворот


Ручками удалять такое замучаешься, плавали, знаем. Запускаем скрипт обнаружения одинаковых кадров, скрипт вываливается с ошибкой Много совпадений подряд. А, ну, естественно: кадры слишком тёмные, поиск сгребает в одну кучу одинаковые и разные кадры. Запускаем скрипт нормализации динамического диапазона, который автоматически делает контрастными границы, чёрное приводит к чёрному, белое к белому, а затем возвращает на место оттенки серого, которые теряются при подобных манипуляциях.
image

Снова запускаем поиск дублей, процесс теперь идёт увереннее, но после удаления ненужных кадров обнаруживается что-то новое. С некоторой периодичностью встречается повторение кадров в обратном порядке. Запускаем исходное видео и смотрим внимательно, ого, да они правда применили хитрость, использующую инерционность зрения, и изображение воспринимается менее дёрганным, чем при обычном дублировании кадров.

image

image

Изменяем скрипт с поиска одинаковых подряд кадров на поиск одинаковых кадров через один промежуточный. Проверяем результаты опять сюрприз: встречается повторение через два кадра. После проверки третьей версии скрипта сюрпризы заканчиваются.

Проблема удаления лишних кадров неожиданно стала очень серьезной. На таких тёмных и не насыщенных деталями кадрах нельзя доверять автоматическому поиску дублей, он ошибётся неоднократно, пропустив ненужное и удалив нужное. Прогоняем поиск всех видов дублей на другом эпизоде фильма, в котором количество ошибок будет минимальным. В случае простого повторения, выделив дубли в менеджере файлов, можно понять схему повторения и удалить лишние файлы программно.
image
В этом же случае шаблон выглядел случайно-периодичным, небольшие куски повторяются, но в целом суть неясна. Что теперь делать? Либо отказываться от всей затеи, либо реверсить алгоритм повторения.

Изменяем скрипт поиска дублей, теперь в имена файлов будут добавляться метки в зависимости от вида дублирования. Переносим весь список файлов в Excel и оставляем только метки, переворачиваем столбец в строку и вылетаем за допустимое количество столбцов, теперь придётся разделить на два листа. Выделяем одним цветом дублированные кадры одного типа, что позволит задействовать биологический анализатор шаблонов.

image

Короткие повторения группируются. Проверяем насколько точно повторяется длинная группа. Группы похожи, но присутствуют небольшие отличия. Это фиаско. Есть несколько возможных причин такой картины: намеренно вносились случайные изменения, или использовалось несколько алгоритмов дополнения кадров, или учитывается содержание кадров, или метод использует не интуитивную функцию. Записывать гораздо более длинную последовательность повторов для математического анализа выглядело как перебор.

image

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

image

Используя итоговый список, удаляем лишнее, проверяем и вуаля, задача выглядит решенной на 9 из 10.

image


Основная работа


Далее следует 17 операций чёрной магии, в ходе которых на диске образуется 17 папок, содержащих кадры видео после каждой манипуляции. Кроме самого оцветнения, производится автоматическая коррекция неудачно оцветнённых кадров, значительное повышение четкости изображения, восстановленному изображению возвращается аналоговость (чтобы избавиться от ощущения фотошопа), для всего этого применяется 5 разных инструментов улучшения изображений, связанных между собой скриптами, которые туда-сюда переливают каналы яркости и цвета. Названия инструментов останутся моей профессиональной тайной, уж извините, слишком много труда и времени потрачено на собирание этого зоопарка и его модифицирование. Когда я увидел результаты работы Deoldify 2, мне стало понятно, что моё желание быть лучшим в этой области лишено смысла, как бы классно я не выжимал процентики качества, каждый новый подобный алгоритм превосходит старое в разы. Я бросил заниматься оцветнением и погрузился в Machine Learning с целью собрать свой Deoldify, правда, потом произошла череда событий, отвлекших меня от этой цели. В итоге я объединил несколько готовых проектов в общий процесс, результаты работы которого, хоть как-то заменяют мне мой несостоявшийся алгоритм оцветненения. Возможно, в следующей статье я расскажу как использовать оцветнялку от Google, если получится обуздать её аппетит к памяти, там будет код и подробности.

Для завершения работы над видео необходимо постараться убрать косяки, для этого лучше всего подходит профессиональный видеокомбайн Davinci Resolve. Если отдельно открыть следующую картинку, то можно разглядеть количество элементов в цепи ретуширования. Эта конструкция делает фон чёрным, приближает цвета к естесственным, борется с лишними цветами, создаёт имитацию луча прожектора (скрывает незначительные следы использования коррекции).

image

Остаётся сделать интерполяцию кадров, косметическое повышение разрешения до 2K, и вот наше видео готово. Исходная картинка слишком тёмная и чудес ждать не приходится, но зато теперь есть возможность рассматривать плавные движения чёткой фигуры танцовщицы.

Сам же фильм содержит множество сцен с относительно хорошей картинкой, что позволяет оценить насколько сильно алгоритмическая обработка может улучшить изображение. Кадры 2K разрешения слишком большие для статьи, поэтому рядом с полным кадром исходного изображения вставлены уменьшенные в 2 раза финальные кадры.

image


верни картинку взад


image


image


image

Итоги


Сюжет про танец содержит 1251 кадр (до интерполяции), работа заняла 5 дней.
Музыка добавлена из библиотеки бесплатной музыки Youtube.

Фильм содержит 19660 кадров (до интерполяции), обрабатывалось 14 дней (только алгоритмы, ручная ретушь не применялась). С музыкой тут было сложнее, сначала был вариант, собранный из кусочков оперы, которая лежит в основе сценария фильма, но из-за авторских прав не получилось опубликовать эту версию, пришлось использовать подходящие композиции из первой найденной библиотеки, говорят, вышло лучше чем в первый раз.

Характеристики ЭВМ: Amd Ryzen 3 1200, 4 Гб ОЗУ, GTX 1060 3 Гб

- Ссылки на мои работы:

Youtube Не.ЧБ

Rutube Не.ЧБ

Instagram
P.s Не смог удержаться, оцветнил :)

Подробнее..

Перевод Оптимизация веб-графики в 2021 году

20.06.2021 18:15:44 | Автор: admin
Изображения, используемые на веб-страницах, привлекают пользователей, пользователи довольно-таки охотно щёлкают по ним мышью. Изображения делают веб-страницы лучше во всём кроме скорости работы страниц. Изображения это огромные куски байтов, которые обычно являются теми частями сайтов, которые загружаются медленнее всего. В этом материале я собрал всё, что нужно знать в 2021 году об улучшении скорости работы веб-страниц через оптимизацию работы с изображениями.



Изображения обычно имеют большие размеры. Даже очень большие. В большинстве случаев CSS- и JavaScript-ресурсы, необходимые для обеспечения работоспособности страниц это мелочь в сравнении с тем объёмом данных, который нужно передать по сети для загрузки изображений, используемых на страницах. Медленные изображения могут повредить показателям Core Web Vitals сайта, могут оказать воздействие на SEO и потребовать дополнительных затрат на трафик. Изображения это обычно тот самый ресурс сайта, который оказывает решающее воздействие на показатель Largest Contentful Paint (LCP) и на задержки загрузки сайта. Они способны увеличить показатель Cumulative Layout Shift (CLS). Если вы не знакомы с этими показателями производительности сайтов почитайте о них в Definitive Guide to Measuring Web Performance.

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

1. Формат изображений


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

Есть одно хорошее общее правило, касающееся выбора форматов для изображений. Заключается оно в том, что для фотографий рекомендуется использовать формат JPG, а для рисунков, схем и прочего подобного формат PNG. Вот пример.


Изображения ленивца

Слева мы можем видеть фото нашего товарища-ленивца Сэма. Эта картинка в формате JPG занимает всего лишь 32,7 Кб. А если то же самое изображение преобразовать в формат PNG размер графического файла увеличится более чем вдвое до 90,6 Кб!

Справа находится рисунок со всё тем же Сэмом. Этот рисунок лучше всего хранить в формате PNG. Так он занимает всего 5,5 Кб. А если преобразовать его в JPG, то его размер подскочит до 11,3 Кб.

Обратите внимание: всё то, что фотографиями не является, обычно занимает меньше места, чем фотографии. Не забывайте об этом, проектируя свои веб-страницы.

Существует, конечно, ещё много графических форматов! Если у вас имеется некое векторное изображение (состоящее из всяческих линий и геометрических фигур), то вам лучше всего подойдёт формат SVG. Более новые браузеры поддерживают и более современные графические форматы вроде AVIF и WebP. Их использование для хранения подходящих изображений позволяет добиться ещё более серьёзного уменьшения размеров графических файлов.

2. Отзывчивые изображения и их пиксельные размеры


Не все посетители сайта будут просматривать его в одних и тех же условиях. У кого-то имеется огромный монитор шириной в 1600 пикселей. А кто-то смотрит сайт на планшете с шириной экрана в 900 пикселей, или на телефоне с экраном шириной в 600 пикселей. Если на сайте применяется изображение шириной в 1200 пикселей это будет означать, что при просмотре такого сайта на устройствах с небольшими экранами сетевые и другие ресурсы будут тратиться впустую, так как размер таких изображений при выводе на экран, всё равно, будет уменьшен.


Просмотр сайта на устройствах с разными экранами

Почему бы не уменьшить пиксельные размеры изображения до загрузки этих изображений пользователями? Подготовив такие изображения, можно воспользоваться HTML-механизмами, нацеленными на работу с отзывчивыми изображениями, и указать, в каких условиях нужно выводить каждое из изображений.

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 900px) 100vw, 1200px"alt="my awesome picture" height="900" width="1200" />

В данном случае ширина базового изображения составляет 1200 пикселей. Оно, кроме того, является изображением, записанным в атрибут src тега и используемым по умолчанию. В srcset описаны 3 варианта изображения шириной в 600, 900 и 1200 пикселей. В sizes используются медиа-запросы CSS, позволяющие дать браузеру подсказку, касающуюся видимой области, доступной для вывода изображения. Если ширина окна меньше 900 пикселей место, где будет выведено изображение, займёт всю его ширину 100vw. В противном случае место для вывода изображения никогда не окажется шире 1200 пикселей.

Большинство инструментов для работы с изображениями, вроде Photoshop, Gimp и Paint.NET, умеют экспортировать изображения в различных размерах. Стандартные системные графические программы тоже, в определённых пределах, способны решать подобные задачи. А если надо автоматизировать обработку очень большого количества изображений возможно, есть смысл взглянуть на соответствующие инструменты командной строки вроде ImageMagick.

Скрытие изображений при просмотре сайта на мобильных устройствах


При работе над некоторыми страницами может быть принято решение, в соответствии с которым изображения, имеющиеся на этих страницах, не будут выводиться при просмотре их на мобильных устройствах. Дело может быть в том, что эти изображения просто слишком велики. При этом установка для изображения стиля display:none особой пользы не принесёт из-за того, что браузер, всё равно, будет тратить время и трафик на загрузку изображения. Вместо этого можно воспользоваться атрибутом sizes, сообщив с его помощью браузеру о том, в каких условиях изображение выводиться не будет.

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 600px) 0, 600px"alt="my awesome picture" height="900" width="1200" />

Тут сказано, что если ширина экрана меньше чем 600 пикселей ширина области для вывода изображения будет равняться 0. В результате браузер, при просмотре сайта на устройстве с маленьким экраном, будет знать о том, что изображение ему выводить не нужно, а значит не нужно и его загружать.

3. Качество изображений


Помимо формата, в котором хранится изображение, и его размеров в пикселях, при сохранении изображений в форматах, поддерживающих сжатие данных с потерями, обычно можно настраивать уровень сжатия или уровень качества изображений. Алгоритмы сжатия изображений с потерями убирают из них некоторые данные, которые (на низких уровнях сжатия) могут и не повлиять на внешний вид изображений, но при этом, всё равно, занимают место. Взгляните на следующий пример.


Исходное PNG-изображение с прозрачными участками имеет размер 57 Кб. Такое же изображение, но сжатое, имеет размер 15 Кб.

Уменьшение размера изображения достигается путём удаления из него неиспользуемых цветов, или путём комбинирования цветов и пикселей, которые очень похожи друг на друга. Но, на самом деле, в тонкости всего этого можно и не вдаваться. Подбор приемлемого уровня сжатия изображений можно доверить специализированным инструментам. Например самые разные задачи по сжатию изображений хорошо решают TinyPNG и ImageMin.

4. Встраивание изображений в веб-страницы


Иногда изображение играет крайне важную роль для обеспечения работоспособности веб-страницы. Например оно может представлять собой кнопку, логотип или какую-нибудь иконку. После того, как размер такого изображения доведён до минимума, единственный способ дальнейшего ускорения его загрузки заключается во встраивании кода этого изображения в код страницы. Это позволит уменьшить количество запросов, необходимых на загрузку материалов страницы, и позволит вывести изображение в ходе рендеринга страницы.

Код изображений встраивают в код страниц, преобразуя данные изображений в base64-строки и помещая то, что получится, в атрибут src тега . Выглядеть это может так, как показано на следующем рисунке.


Изображение, встроенное в страницу

Может, выглядит это и странновато, но тут перед нами так называемый Data URL. Такие URL пользуются полной поддержкой всех браузеров. В атрибуте src сказано, что соответствующие данные надо воспринимать как PNG-изображение в кодировке base64. После описания изображения идёт набор символов, представляющих содержимое этого изображения. В данном случае это маленькая красная точка.

Google часто использует встроенные в страницу изображения при выводе результатов поиска. Это одна из причин того, что поиск Google кажется таким быстрым.

Вот материал, в котором проанализированы подходы к повышению производительности веб-ресурсов, используемые Google. Возможно, вы найдёте тут что-то такое, что вам пригодится.

А вот удобный веб-инструмент для преобразования изображений в формат base64.

5. Ленивая загрузка изображений


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

Вместо того, чтобы заставлять браузер сразу загружать все изображения, можно посоветовать ему немного полениться. Ленивая загрузка изображений это такой подход к работе с изображениями, когда браузеру предлагают загружать изображения только тогда, когда они могут понадобиться пользователю. Применение ленивой загрузки изображений способно оказать огромное положительное влияние на показатель Largest Contentful Paint (LCP), так как благодаря этому браузер, при загрузке страницы, может уделить основное внимание только самым важным изображениям.

В спецификации HTML есть атрибут loading, которым можно воспользоваться для организации ленивой загрузки изображений, но он пока имеет статус экспериментального. А Safari ещё даже его не поддерживает. В результате о его широком использовании говорить пока нельзя. Но, к счастью, ленивую загрузку изображений можно организовать средствами JavaScript.

Существуют соответствующие библиотеки. Например lazysizes. В этой библиотеке обрабатываются различные пограничные случаи, она уделяет немало внимания вопросам совместимости с разными браузерами. Но, в целом, код, ответственный за ленивую загрузку изображений, выглядит так, как показано ниже.

var lazyEls = [].slice.call(document.querySelectorAll("[data-src]"));var lazyObserver = new IntersectionObserver(function(entries) {entries.forEach(function(entry) {if (entry.isIntersecting) {var el = entry.target;var src = el.getAttribute("data-src");if (src) { el.setAttribute("src", src); }lazyObserver.unobserve(el);}});});lazyEls.forEach(function(el) {lazyObserver.observe(el);});

Тут, для определения того момента, когда надо загружать изображение, используется объект IntersectionObserver. Когда наступает нужный момент содержимое атрибута data-src копируется в атрибут src и изображение загружается. Тот же подход можно применить к атрибуту srcset и воспользоваться им при работе с любым количеством изображений.

Пользуются этим, переименовывая атрибут src в data-src.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy" />

В этом фрагменте HTML-разметки используется и экспериментальный атрибут loading, и обычный атрибут, который можно использовать с некоей JavaScript-функцией или с какой-нибудь библиотекой.

Настройка размеров области, которую займёт изображение


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

Избежать сдвига макета страницы можно, указав атрибуты height и width тега <img>.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy"width="1200" height="900" />

Обратите на то, что значения атрибутов height и width это не 1200px и 900px. Это просто 1200 и 900. И работают они немного не так, как можно было бы ожидать. Размер соответствующего изображения не обязательно будет составлять 1200x900 пикселей. Этот размер будет зависеть от CSS и от размеров макета страницы. Но браузер, благодаря этим атрибутам, получит сведения о соотношении сторон изображения. В результате, узнав ширину изображения, браузер сможет правильно настроить его высоту.

То есть, например, если макет страницы имеет в ширину всего 800px, то браузер, не загружая изображение, будет знать о том, что ему надо зарезервировать 600px вертикального пространства для вывода изображения с правильным соотношением сторон.

Итоги


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

Как вы оптимизируете изображения, используемые в ваших веб-проектах?


Подробнее..

Защищаем сканы своих документов в интернет

13.05.2021 22:20:41 | Автор: admin

В какой-то момент начались непонятные звонки на сотовый номер, с "привлекательными" предложениями разбогатеть или взять взаймы без процентов ). Один раз даже убеждали что я должен немедленно расплатиться по долгу (которого нет). Заинтересовавшись своей кредитной историей (с чего вдруг такая активность), решил узнать, какая она.

Чтобы узнать в каких кредитных бюро есть записи относящиеся к Вашей персоне, необходимо заполнить заявку на сайте госуслуг (скорей всего это не единственный способ). Получив через некоторое время .PDF файлы из 3-х контор (столько организаций выдал госпортал). Выяснил что, по скану моего паспорта запрашивалась кредитная история от "микрофинансовых" организаций и не один раз. Всё без моего ведома, ибо не пользуюсь подобными услугами.

Финансовых рисков по объективным причинам я не усмотрел, но решил обезопасить себя от аналогичных ситуациях в будущем. Или хотя бы знать канал утечки (что маловероятно в силу способа защиты и уверенности что злодеи не дураки, и не будут использовать такой скан).

Паспорт у меня был ветхий (любил водные процедуры)) и был поменян на новый, на тех же госуслугах по электронному заявлению. Обошлось мне это примерно в 1000+ р. и часть моего времени для похода в МФЦ. Получив новый паспорт, отсканировал все его страницы. Теперь при запросе какой-либо организации скана, отправляю скан с уникальным (название организации) водяным знаком.

Крайне не рекомендую пользоваться онлайн-сервисами по нанесению водяных знаков на графический файл, по понятным причинам!!!

Вы можете сами поискать подходящий вариант. Первый раз вообще рисовал в Paint, кистью, не задевая информационную часть. Потом нашёл программу VisualWatermark. Бесплатная версия добавляет своё в нижней кромке рисунка, но если рисунок сделать чуть больше, то потом рекламную часть можно обрезать.

В конце статьи скан паспорта для Налоговой обработанный с помощью Paint. И для РосРеестра обработанный с помощью вышеназванной программы с обязательной для демо-версии надписью внизу. Не сочтите за рекламу, просто стараюсь быть максимально объективным.

Буду рад если это будет в помощь и поможет избежать проблем! Спасибо за внимание!

p.s. Программе делающей знаки лучше запретить доступ в сеть.

Подробнее..

Сим-сим откройся как я научил дверь своего подъезда узнавать меня в лицо

07.06.2021 18:08:54 | Автор: admin

Пятничный рабочий день на удалёнке уже подходил к концу, как в дверь постучали, чтобы сообщить об установке нового домофона. Узнав, что новый домофон имеет мобильное приложение, позволяющее отвечать на звонки не находясь дома, я заинтересовался и сразу же загрузил его на свой телефон. Залогинившись, я обнаружил интересную особенность этого приложения даже без активного вызова в мою квартиру я мог смотреть в камеру домофона и открывать дверь в произвольный момент времени. "Да это же онлайн АРI к двери подъезда!" щёлкнуло в голове. Судьба предстоящих выходных была предрешена.

Видеодемонстрация в конце статьи.

Кадр из фильма Пятый ЭлементКадр из фильма Пятый Элемент

Дисклеймер

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

Получение API

Чтобы автоматизировать управление дверью, необходимо понять, куда и в каком формате отправляет запросы само приложение. Реверс-инжиниринг дело неоднозначное, поэтому я попытался обойтись без него и вместо этого просто перехватить свой собственный трафик. Для этой задачи я взял НТТР Тооlkit комплекс программ, который позволяет наладить прослушивание http(s) запросов собственного Android устройства.

Первая попытка оказывается провальной после установки на телефон Android-части инструментария и сгенерированного Certificate authority оказалось, что мобильное приложение домофона не доверяет пользовательским СА. Согласно документации, начиная с Android 7 манифест приложения должен явно изъявлять такое желание.

Так как мой телефон не поддерживает root доступ для модификации списка системных СА, я воспользовался официальным эмулятором Android, идущим в комплекте с Android Studio. После запуска эмулятора и перехвата с помощью ADB меня встретило радостное сообщение о том, что трафик от всех приложений без Certificate pinning будет успешно расшифрован.

Успешное соединение с HTTP ToolkitУспешное соединение с HTTP Toolkit

К счастью, приложение оказалось как раз из таких немного побродив по приложению и открыв дверь, можно переходить к анализу логов.

Запрос открытия двериЗапрос открытия двери

Всего интересными показалось три запроса:

  1. Запрос на открытие двери подъезда: POST по адресу /rest/v1/places/{place_id}/accesscontrols/{control_id}/actions с JSON-телом {"name": "accessControlOpen"}

  2. Получение снимка (превью) с камеры: GET по адресу /rest/v1/places/{place_id}/accesscontrols/{control_id}/videosnapshots

  3. Получение ссылки на видеопоток с аудио: GET по адресу /rest/v1/forpost/cameras/{camera_id}/video?LightStream=0

HTTP заголовки всех трёх запросов содержат ключ Authorization судя по всему, именно по нему происходит авторизация при выполнении запросов. Сделав пару запросов руками через Advanced REST Client, чтобы убедиться, что заголовка Authorization достаточно и в самостоятельной работе с API не осталось подводных камней, я понял, что можно откладывать эмулятор и переходить к написанию кода.

Вооружившись Python и requests, я написал необходимые для следующих этапов функции исполнения каждого из трёх действий:

HEADERS = {"Authorization": "Bearer ###"}ACTION_URL = "https://###.ru/rest/v1/places/###/accesscontrols/###/"VIDEO_URL = "https://###.ru/rest/v1/forpost/cameras/###/video?LightStream=0"def get_image():    result = requests.get(f'{ACTION_URL}/videosnapshots', headers=HEADERS)    if result.status_code != 200:        logging.error(f"Failed to get an image with status code {result.status_code}")        return None    logging.warning(f"Image received successfully in {result.elapsed.total_seconds()}sec")    return result.contentdef open_door():    result = requests.post(        f'{ACTION_URL}/actions', headers=HEADERS, json={"name": "accessControlOpen"})    if result.status_code != 200:        logging.error(f"Failed to open the door with status code {result.status_code}")        return False    logging.warning(f"Door opened successfully in {result.elapsed.total_seconds()}sec")    return Truedef get_videostream_link():    result = requests.get(VIDEO_URL, headers=HEADERS)    if result.status_code != 200:        logging.error(f"Failed to get stream link with status code {result.status_code}")        return False    logging.warning(f"Stream link received successfully in {result.elapsed.total_seconds()}sec")    return result.json()['data']['URL']

Поиск знакомых лиц в кадре

Прежде чем начать этот раздел, нужно рассказать пару слов об уже имеющихся на тот момент у меня в распоряжении серверных мощностях это недорогая виртуальная машина с доступом ко всего одному потоку Intel(R) Xeon(R) CPU E5-2650L v3 @ 1.80GHz, 1GB оперативной памяти и 0 GPU. Тратиться на более дорогую конфигурацию не хотелось, а значит, нужно было попробовать выжать максимум из имеющихся ресурсов.

На данном этапе проектом заинтересовалась моя жена, хорошо разбирающаяся в машинном обучении и в итоге взявшая на себя эту задачу. Для этой работы был выбран OpenVINO Toolkit инструментарий от Intel, в том числе заточенный как раз на запуск моделей машинного обучения на CPU.

Непродолжительный поиск существующих решений привёл на страницу Interactive Face Recognition Demo официального демо, показывающего ровно необходимый функционал сравнения видимых в кадре лиц с базой заранее сохранённых. Единственная проблема состояла в том, что данный пример по каким-то причинам исчез после релиза 2020.3, а удобная установка пакета через pip у проекта появилась только с 2021.1. Было решено установить последнюю версию OpenVINO и адаптировать код под неё.

К счастью, гит помнит всё и получить из репозитория проекта код демо и нужные модели не составило труда. После удаления всей работы с командной строкой (взяв для всего значения по умолчанию), визуализацией и видеопотоком вебкамеры, был получен класс, способный распознавать лица в конкретном кадре:

class ImageProcessor:    def __init__(self):        self.frame_processor = FrameProcessor()    def process(self, image):        detections = self.frame_processor.process(image)        labels = []        for roi, landmarks, identity in zip(*detections):            label = self.frame_processor.face_identifier.get_identity_label(                identity.id)            labels.append(label)        return labels

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

100 runs on an image with known face:Total time: 7.356sTime per frame: 0.007sFPS: 135.944100 runs on an image without faces:Total time: 2.985sTime per frame: 0.003sFPS: 334.962

Очевидно, что показанная производительность с запасом покрывает нужды детектирования лиц в реальном времени.

1 FPS: Работа со снимками с камеры

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

class ImageProcessor:# <...>    def process_single_image(self, image):        nparr = np.fromstring(image, np.uint8)        img_np = cv2.imdecode(nparr, cv2.IMREAD_COLOR)        labels = self.process(img_np)        return labelsdef snapshot_based_intercom_id():    processor = ImageProcessor()    last_open_door_time = time.time()    while True:        start_time = time.time()        image = get_image()        result = processor.process_single_image(image)        logging.info(f'{result} in {time.time() - start_time}s')        # Successfull detections are "face{N}"        if any(['face' in res for res in result]):            if start_time - last_open_door_time > 5:                open_door()                with open(f'images/{start_time}_OK.jfif', 'wb') as f:                    f.write(image)                last_open_door_time = start_time

Цикл получает картинки от домофона, ищет на них знакомые лица, открывает дверь в случае успешной детекции и сохраняет фото на память. Важно было не забыть ограничить частоту отправки команды открытия двери, т.к. лицо обычно распознаётся ещё на подходах к двери.

Момент успешного распознавания, версия с обработкой отдельных снимковМомент успешного распознавания, версия с обработкой отдельных снимков

Заработало! Полностью довольный первым запуском, я вернулся в квартиру. Единственное, что портило впечатление от системы распознавания время реакции на появление лица в кадре, т.к. время отклика API оставляло желать лучшего. Низкая частота поступления данных, 0.7с на получение картинки и 0.6с на открытие двери, давали видимый невооружённым взглядом лаг.

До 30 FPS: Обработка видеопотока

Получить кадры из видео оказалось на удивление просто:

vcap = cv2.VideoCapture(link)success, frame = vcap.read()

Замеры показали, что камера домофона способна выдавать стабильные 30 FPS. Проблемой оказалось поддержание меньшей частоты кадров: метод read() возвращает последний необработанный кадр из внутренней очереди кадров. Если эта очередь наполняется видеопотоком быстрее, чем вычитывается, то обрабатываемые кадры начинают отставать от реального времени, что приводит к нежелательной задержке. Дополнительно, с практической точки зрения, распознавать лица 30 раз в секунду пустая трата вычислительных ресурсов, так как обычно люди подходят к двери подъезда с небольшой скоростью.

Первым потенциальным решением было установить размер внутренней очереди: vcap.set(CV_CAP_PROP_BUFFERSIZE, 0);. Согласно найденной информации, такой трюк должен был хорошо работать с любой конфигурацией системы для версий OpenCV выше 3.4, но по какой-то причине, так и не оказал никакого влияния в моём случае. Единственной рабочей альтернативой стал подход, описанный в этом ответе со StackOverflow завести отдельный поток, читающий кадры из камеры на максимально возможной скорости и сохраняющий последний в поле класса для дальнейшего доступа (впоследствии оказалось, что именно этот цикл ответственен за большую часть потребления процессора).

Получилась модификация ImageProcessor для обработки видеопотока с частотой 3 кадра в секунду:

class CameraBufferCleanerThread(threading.Thread):    def __init__(self, camera, name='camera-buffer-cleaner-thread'):        self.camera = camera        self.last_frame = None        self.finished = False        super(CameraBufferCleanerThread, self).__init__(name=name)        self.start()    def run(self):        while not self.finished:            ret, self.last_frame = self.camera.read()    def __enter__(self): return self    def __exit__(self, type, value, traceback):        self.finished = True        self.join()class ImageProcessor:# <...>    def process_stream(self, link):        vcap = cv2.VideoCapture(link)        interval = 0.3 # ~3 FPS        with CameraBufferCleanerThread(vcap) as cam_cleaner:            while True:                frame = cam_cleaner.last_frame                if frame is not None:                    yield (self.process(frame), frame)                else:                    yield (None, None)                time.sleep(interval)

И соответствующая модификация snapshot_based_intercom_id:

def stream_based_intercom_id():    processor = ImageProcessor()    link = get_videostream_link()    # To notify about delays    last_time = time.time()    last_open_door_time = time.time()    for result, np_image in processor.process_stream(link):        current_time = time.time()        delta_time = current_time - last_time        if delta_time < 1:            logging.info(f'{result} in {delta_time}')        else:            logging.warning(f'{result} in {delta_time}')        last_time = current_time        if result is None:            continue        if any(['face' in res for res in result]):            if current_time - last_open_door_time > 5:                logging.warning(                  f'Hey, I know you - {result[0]}! Opening the door...')                last_open_door_time = current_time                open_door()                cv2.imwrite(f'images/{current_time}_OK.jpg', np_image)

Тест в реальных условиях показал ощутимое падение времени отклика при хорошем освещении подъездная дверь оказывалась открытой ещё до того, как я успевал подойти к ней вплотную.

Момент успешного распознавания, версия с обработкой видеопотокаМомент успешного распознавания, версия с обработкой видеопотока

Управление с помощью Telegram бота

Сама система доказала свою работоспособность и теперь хотелось создать для неё удобный интерфейс для включения/выключения. Телеграм бот показался отличным вариантом решения для этой задачи.

С помощью пакета python-telegram-bot была написана простая оболочка, принимающая в себя callback включения/выключения системы и список доверенных никнеймов.

class TelegramInterface:    def __init__(self, login_whitelist, state_callback):        self.state_callback = state_callback        self.login_whitelist = login_whitelist        self.updater = Updater(            token = "###", use_context = True)        self.run()    def run(self):        dispatcher = self.updater.dispatcher        dispatcher.add_handler(CommandHandler("start", self.start))        dispatcher.add_handler(CommandHandler("run", self.run_intercom))        dispatcher.add_handler(CommandHandler("stop", self.stop_intercom))        self.updater.start_polling()    def run_intercom(self, update: Update, context: CallbackContext):        user = update.message.from_user        update.message.reply_text(            self.state_callback(True) if user.username in self.login_whitelist else 'not allowed',            reply_to_message_id=update.message.message_id)    def stop_intercom(self, update: Update, context: CallbackContext):        user = update.message.from_user        update.message.reply_text(            self.state_callback(False) if user.username in self.login_whitelist else 'not allowed',            reply_to_message_id=update.message.message_id)    def start(self, update: Update, context: CallbackContext) -> None:        update.message.reply_text('Hi!')                class TelegramBotThreadWrapper(threading.Thread):    def __init__(self, state_callback, name='telegram-bot-wrapper'):        self.whitelist = ["###", "###"]        self.state_callback = state_callback        super(TelegramBotThreadWrapper, self).__init__(name=name)        self.start()    def run(self):        self.bot = TelegramInterface(self.whitelist, self.state_callback)

И следующая ступень эволюции функции intercom_id, обрабатывающая синхронизацию между потоками обработки данных и получения команд от бота:

def stream_based_intercom_id_with_telegram():    processor = ImageProcessor()    loop_state_lock = threading.Lock()    loop_should_run = False    loop_should_change_state_cv = threading.Condition(loop_state_lock)    is_loop_finished = True    loop_changed_state_cv = threading.Condition(loop_state_lock)    def stream_processing_loop():        nonlocal loop_should_run        nonlocal loop_should_change_state_cv        nonlocal is_loop_finished        nonlocal loop_changed_state_cv        while True:            with loop_should_change_state_cv:                loop_should_change_state_cv.wait_for(lambda: loop_should_run)                is_loop_finished = False                loop_changed_state_cv.notify_all()                logging.warning(f'Loop is started')            link = get_videostream_link()            last_time = time.time()            last_open_door_time = time.time()            for result, np_image in processor.process_stream(link):                with loop_should_change_state_cv:                    if not loop_should_run:                        is_loop_finished = True                        loop_changed_state_cv.notify_all()                        logging.warning(f'Loop is stopped')                        break                current_time = time.time()                delta_time = current_time - last_time                if delta_time < 1:                    logging.info(f'{result} in {delta_time}')                else:                    logging.warning(f'{result} in {delta_time}')                last_time = current_time                if result is None:                    continue                if any(['face' in res for res in result]):                    if current_time - last_open_door_time > 5:                        logging.warning(f'Hey, I know you - {result[0]}! Opening the door...')                        last_open_door_time = current_time                        open_door()                        cv2.imwrite(f'images/{current_time}_OK.jpg', np_image)    def state_callback(is_running):        nonlocal loop_should_run        nonlocal loop_should_change_state_cv        nonlocal is_loop_finished        nonlocal loop_changed_state_cv        with loop_should_change_state_cv:            if is_running == loop_should_run:                return "Intercom service state is not changed"            loop_should_run = is_running            if loop_should_run:                loop_should_change_state_cv.notify_all()                loop_changed_state_cv.wait_for(lambda: not is_loop_finished)                return "Intercom service is up"            else:                loop_changed_state_cv.wait_for(lambda: is_loop_finished)                return "Intercom service is down"    telegram_bot = TelegramBotThreadWrapper(state_callback)    logging.warning("Bot is ready")    stream_processing_loop()

Результат

Видео:

Послесловие

Несмотря на возможности, которые технология умных домофонов несёт жильцам, объединённые в единую сеть сотни (тысячи?) подъездных дверей с камерами и микрофонами (да, в произвольно получаемом видеопотоке есть и аудио!), ведущими непрерывную запись как по мне, скорее пугающее явление, открывающее новые возможности для нарушения приватности.

Я бы предпочёл, чтобы доступ к видеопотоку предоставлялся только в момент звонка в квартиру и ведущаяся трёхдневная запись, позиционирующаяся как средство раскрытия правонарушений, хранилась не на серверах компании, а непосредственно в домофоне, с возможностью доступа к ней по запросу. Или не велась вовсе.

Подробнее..

Создание нейронной сети Хопфилда на JavaScript

05.06.2021 18:10:05 | Автор: admin

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

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

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

Исходникина Github и демо.

Дляреализациипонадобится:

  • Браузер

  • Базовоепониманиенейросетей

  • БазовыезнанияJavaScript/HTML

Немноготеории

Нейронная сеть Хопфилда (англ. Hopfield network) полносвязная нейронная сеть ссимметричной матрицей связей. Такая сеть может быть использована для организации ассоциативной памяти, как фильтр, атакже для решения некоторых задач оптимизации.

Сеть Хопфилда является абсолютно однородной структурой без какой-либо внутренней специализации еенейронов. Еёклассический вариант состоит изединственного слоя нейронов, число которых является одновременно числом входов ивыходов сети. Каждый нейрон сети связан совсеми остальными нейронами, атакже имеет один вход, через который осуществляется ввод сигнала.

Структурная схема нейросети ХопфилдаСтруктурная схема нейросети Хопфилда

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

Алгоритм работы сети:

  1. Инициализация
    Веса нейронов устанавливаются по следующей формуле:

    w_{ij}=\left\{\begin{matrix} \sum_{k=1}^{m} x_{i}^{k} * x_{j}^{k} & i \neq j \\0, & i=j \end{matrix}\right.

    где m количество образов
    x_{i}^{k}, x_{j}^{k} i - ый и j - ый элементы вектора k - ого образца.

  2. Навходы сети подается неизвестный сигнал. Фактически его ввод осуществляется непосредственной установкой значений выходов:
    y_{j}(0) = x_{j}

  3. Рассчитывается выход сети (новое состояние нейронов иновые значения выходов):

    y_{j}(t+1)=f\left ( \sum_{i=1}^{n} w_{ij}*y_{i}(t)\right )

    где f пороговая активационная функция собластью значений [-1; 1];
    t номер итерации;
    j = 1...n; n количество входов инейронов.

  4. Проверка изменения выходных значений запоследнюю итерацию. Если выходы изменились переход кпункту3, иначе, если выходы стабилизировались, завершение функционирования. При этом выходной вектор представляет собой образец, наилучшим образом сочетающийся свходными данными.

Разработка

Визуальная часть

Для начала посмотрим как работает итоговый проект.

Демонстрация работы программыДемонстрация работы программы

Онсостоит издвух элементов Canvas итрех кнопок. Это простейший HTML иCSS код, ненуждающийся впояснении (можете скопировать сгитхаба).

Левый элемент Canvas нужен для рисования изображений, которые затем будут использованы для обучения (кнопка Запомнить) или распознавания нейросети. Направом элементе отображается результат распознавания сигнала, находящегося наданный момент налевом Canvas (вданном случае сеть вспомнила буквуТ наоснове искаженного сигнала).

Здесь нужно обратить внимание нато, что область для рисования представлена сеткой 1010 ипозволяет закрашивать клетки только черным цветом. Так как всети Хопфилда число нейронов равно числу входов, количество нейронов будет равно длине входного сигнала, тоесть 100 (унас всего 100 клеток наэкране). Входной сигнал при этом будет двоичным массив, состоящий из1и1, где 1 это белый, а1 черный цвет.

Наконец-то приступим кнаписанию кода, сначала инициализируем необходимые переменные.

Код инициализации
// Размер сетки установим равным 10 для простоты тестированияconst gridSize = 10;// Размер одного квадрата в пикселяхconst squareSize = 45;// Размер входного сигнала (100)const inputNodes = gridSize * gridSize;// Массив для хранения текущего состояния картинки в левом канвасе,// он же является входным сигналом сетиlet userImageState = [];// Для обработки движений мыши по канвасуlet isDrawing = false;// Инициализация состоянияfor (let i = 0; i < inputNodes; i += 1) {    userImageState[i] = -1;  }// Получаем контекст канвасов:const userCanvas = document.getElementById('userCanvas');const userContext = userCanvas.getContext('2d');const netCanvas = document.getElementById('netCanvas');const netContext = netCanvas.getContext('2d');

Реализуем функцию рисования сетки, используя инициализированные ранее переменные.

Функция отрисовки сетки
// Функция принимает контекст канваса и рисует// сетку в 100 клеток (gridSize * gridSize)const drawGrid = (ctx) => {  ctx.beginPath();  ctx.fillStyle = 'white';  ctx.lineWidth = 3;  ctx.strokeStyle = 'black';  for (let row = 0; row < gridSize; row += 1) {    for (let column = 0; column < gridSize; column += 1) {      const x = column * squareSize;      const y = row * squareSize;      ctx.rect(x, y, squareSize, squareSize);      ctx.fill();      ctx.stroke();    }  }  ctx.closePath();};

Чтобы оживить полученную сетку, добавим обработчики клика идвижения мыши поканвасу.

Обработчики движений мыши
// Обработка клика мышиconst handleMouseDown = (e) => {  userContext.fillStyle = 'black';  // Рисуем залитый прямоугольник в позиции x, y  // размером squareSize х squareSize (45х45 пикселей)  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  // На основе координат вычисляем индекс,  // необходимый для изменения состояния входного сигнала  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  // Проверяем необходимо ли изменять этот элемент сигнала  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }  // Изменяем состояние (для обработки движения мыши)  isDrawing = true;};// Обработка движения мыши по канвасуconst handleMouseMove = (e) => {  // Если не рисуем, т.е. не было клика мыши по канвасу, то выходим из функции  if (!isDrawing) return;  // Далее код, аналогичный функции handleMouseDown  // за исключением последней строки isDrawing = true;  userContext.fillStyle = 'black';  userContext.fillRect(    Math.floor(e.offsetX / squareSize) * squareSize,    Math.floor(e.offsetY / squareSize) * squareSize,    squareSize, squareSize,  );  const { clientX, clientY } = e;  const coords = getNewSquareCoords(userCanvas, clientX, clientY, squareSize);  const index = calcIndex(coords.x, coords.y, gridSize);  if (isValidIndex(index, inputNodes) && userImageState[index] !== 1) {    userImageState[index] = 1;  }};

Как вымогли заметить, обработчики использует некоторые вспомогательные функции, такие как getNewSquareCoords, calcIndex иisValidIndex. Ниже код этих функций скомментариями.

Вспомогательные функции
// Вычисляет индекс для изменения в массиве// на основе координат и размера сеткиconst calcIndex = (x, y, size) => x + y * size;// Проверяет, помещается ли индекс в массивconst isValidIndex = (index, len) => index < len && index >= 0;// Генерирует координаты для закрашивания клетки в пределах // размера сетки, на выходе будут значения от 0 до 9const getNewSquareCoords = (canvas, clientX, clientY, size) => {  const rect = canvas.getBoundingClientRect();  const x = Math.ceil((clientX - rect.left) / size) - 1;  const y = Math.ceil((clientY - rect.top) / size) - 1;  return { x, y };};

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

Функция очистки сетки
const clearCurrentImage = () => {  // Чтобы убрать закрашенные клетки, просто заново отрисовываем   // всю сетку и сбрасываем массив входного сигнала  drawGrid(userContext);  drawGrid(netContext);  userImageState = new Array(gridSize * gridSize).fill(-1);};

Теперь можно переходить кразработке мозга программы.

Реализация алгоритма нейросети

Первый этап инициализация сети. Добавим переменную для хранения значений весов нейронов инемного изменим цикл инициализации состояния изображения (входного сигнала).

Инициализация весов сети
...const weights = [];  // Массив весов сетиfor (let i = 0; i < inputNodes; i += 1) {  weights[i] = new Array(inputNodes).fill(0); // Создаем пустой массив и заполняем его 0  userImageState[i] = -1;}...

Так как каждый нейрон всети Хопфилда связан совсеми остальными нейронами, веса сети представлены двумерным массивом, каждый элемент которого является одномерным массивом размером inputNodes элементов. Витоге мыполучаем 100нейронов, укаждого изкоторых по100связей.

Теперь реализуем обработку входного сигнала (изменение весов) нейросетью согласно формуле изпервого шага алгоритма. Данный процесс происходит понажатию накнопку Запомнить. Запомненные образы будут является эталонами для последующего восстановления.

Код обработки входного сигнала
const memorizeImage = () => {  for (let i = 0; i < inputNodes; i += 1) {    for (let j = 0; j < inputNodes; j += 1) {      if (i === j) weights[i][j] = 0;      else {        // Напоминаю, что входной сигнал находится в массиве userImageState и является        // набором -1 и 1, где -1 - это белый, а 1 - черный цвет клеток на канвасе        weights[i][j] += userImageState[i] * userImageState[j];      }    }  }};

Запомнив один или более образов, можно попробовать подать навход сети искаженный образ, либо сильно похожий наодин изних, ипопробовать распознать его. Напишем еще одну функцию:

Функция распознавания искаженного сигнала
// Где-то в html подключаем библиотеку lodash:<script src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>...const recognizeSignal = () => {  let prevNetState;  // На вход сети подается неизвестный сигнал. Фактически   // его ввод осуществляется непосредственной установкой значений выходов  // (2 шаг алгоритма), просто копируем массив входного сигнала  const currNetState = [...userImageState];  do {    // Копируем текущее состояние выходов, // т.е. теперь оно становится предыдущим состоянием    prevNetState = [...currNetState];    // Рассчитываем выход сети согласно формуле 3 шага алгоритма    for (let i = 0; i < inputNodes; i += 1) {      let sum = 0;      for (let j = 0; j < inputNodes; j += 1) {        sum += weights[i][j] * prevNetState[j];      }      // Рассчитываем выход нейрона (пороговая ф-я активации)      currNetState[i] = sum >= 0 ? 1 : -1;    }    // Проверка изменения выходов за последнюю итерацию    // Сравниваем массивы при помощи ф-ии isEqual  } while (!_.isEqual(currNetState, prevNetState));  // Если выходы стабилизировались (не изменились), отрисовываем восстановленный образ  drawImageFromArray(currNetState, netContext);};

Здесь для сравнения выходов сети напредыдущем итекущем шаге используется функция isEqual избиблиотеки lodash.

Для отрисовки полученного сигнала используется функция drawImageFromArray. Она преобразует выходной сигнал сети вдумерный массив иотрисовывает его направом канвасе.

Функция отрисовки изображения из массива точек
const drawImageFromArray = (data, ctx) => {  const twoDimData = [];  // Преобразуем одномерный массив в двумерный  while (data.length) twoDimData.push(data.splice(0, gridSize));  // Предварительно очищаем сетку  drawGrid(ctx);  // Рисуем изображение по координатам (индексам массива)  for (let i = 0; i < gridSize; i += 1) {    for (let j = 0; j < gridSize; j += 1) {      if (twoDimData[i][j] === 1) {        ctx.fillStyle = 'black';        ctx.fillRect((j * squareSize), (i * squareSize), squareSize, squareSize);      }    }  }};

Финальные приготовления

Для полноценного запуска программы осталось только добавить наши функции вкачестве обработчиков для элементов HTML ивызвать функции отрисовки сетки.

Привязываем функции к HTML элементам
const resetButton = document.getElementById('resetButton');const memoryButton = document.getElementById('memoryButton');const recognizeButton = document.getElementById('recognizeButton');// Вешаем слушатели на кнопкиresetButton.addEventListener('click', () => clearCurrentImage());memoryButton.addEventListener('click', () => memorizeImage());recognizeButton.addEventListener('click', () => recognizeSignal());// Вешаем слушатели на канвасыuserCanvas.addEventListener('mousedown', (e) => handleMouseDown(e));userCanvas.addEventListener('mousemove', (e) => handleMouseMove(e));// Перестаем рисовать, если кнопка мыши отпущена или вышла за пределы канвасаuserCanvas.addEventListener('mouseup', () => isDrawing = false);userCanvas.addEventListener('mouseleave', () => isDrawing = false);// Отрисовываем сеткуdrawGrid(userContext);drawGrid(netContext);

Демонстрация работы нейросети

Обучим сеть двум ключевым образам, буквам Т и Н:

Эталонные образы для обучения сетиЭталонные образы для обучения сети

Теперь проверим работу сети на искаженных образах:

Попытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы НПопытка распознать искаженный образ буквы ТПопытка распознать искаженный образ буквы Т

Программа работает! Сеть успешно восстановила исходные образы.

В заключение стоит отметить, что для сети Хопфилда число запоминаемых образов mнедолжно превышать величины, примерно равной 0.15 * n(где n размерность входного сигнала иколичество нейронов). Кроме того, если образы имеют сильное сходство, то они, возможно, будут вызывать усети перекрестные ассоциации, тоесть предъявление навходы сети вектораА приведет кпоявлению наеевыходах вектораБ инаоборот.

Исходникина Github и демо.

Вместо литературы использовались лекции отличного преподавателя понейронным сетям Рощина Сергея Михайловича, зачто ему большое спасибо.

Подробнее..

Motion Amplification или диагностика состояния промышленного оборудования и сооружений с помощью видеоаналитики

10.05.2021 12:10:00 | Автор: admin

Motion Amplification (англ.) усиление движения.

Прямых доказательств, связывающих рождение технологии Motion Amplification с силовыми ведомствами США, у нас нет, но косвенных достаточно. Не случайно среди примеров использования есть немало кейсов из аэрокосмической и оборонной отраслей. Измерение уровня вибрации вертолета во время полета важная, но очень непростая задача. С Motion Amplification она решается довольно быстро и просто

Диагностика состояния вертолета во время полета - YouTube

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

История

В конце 2014 года изобретатель Jeff Hay (основатель компании RDI Technologies) получил два патента под названием Бесконтактный мониторинг состояния мостов и гражданских сооружений и Аппарат и метод визуализации периодических движений механических компонентов. Пытливые умы могут почитать подробные материалы по ссылке, а для остальных кратко расскажем в чем заключается основная идея изобретения.

Если взять дорогую высокочастотную камеру и записать видео (например, тысячу кадров в секунду), то даже за 5 секунд мы получим внушительный массив данных. При движении объекта в объективе камеры происходят изменения пикселей изображений во время записи. Зная частоту съемки (fps), фокусное расстояние объектива и расстояние от камеры до объекта, можно выполнить точные измерения амплитуды движения. Фиксируя даже незначительное движение объекта в кадре, проприетарные алгоритмы при обработке видео усиливают это движение, делая его заметным каждому.

Воплощение идеи потребовало серьезных научных и практических изысканий, и первое решение под названием IRIS M появилось на рынке только в сентябре 2016 года. Функционал программного обеспечения версии 1.0 был прост запись видео и усиление движения. Только в декабре 2016 года (v.1.1) появилась возможность делать измерения. Несмотря на ограниченный функционал, экспертное сообщество и крупные корпоративные заказчики в США встретили новинку с большим энтузиазмом. Решение получило признание American Society of Civil Engineers в 2016г., а также было отмечено вторым призом престижного конкурса Vision Systems Design в 2017г.

Секрет успеха

Получить признание у консервативной и взыскательной аудитории, тем более в короткие сроки, рядовому изобретению явно не по силам. IRIS M объединил в себе плюсы динамично развивающихся технологий видеоаналитики и достоинства традиционной вибродиагностики. Возможность бесконтактным способом оценить состояние объекта, оперативно выполнить измерения и показать реальную картину окружающим оказалась востребованной заказчиками.

Давайте сравним процесс оценки состояния обычного насоса традиционным способом и с помощью IRIS M. Сбор данных традиционным способом осуществляется обычно с помощью виброанализатора по ограниченному набору точек контроля.

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

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

Motion Amplification (МА) сочетает в себе преимущества традиционной вибродиагностики, фазового анализа и использования специализированного программного обеспечения ODS (Operational Deflection Shape) для анимации:

  • можно собрать в тысячи раз больше данных, чем традиционным способом;

  • все данные собираются одновременно, а не последовательно

  • данные для анализа фаз собираются за одну съемку, а не отдельно (как при традиционном подходе)

  • каждая точка изображения может быть использована для измерения уровня вибрации

  • обработанное видео является наглядным отчетом, понятным людям даже без специализированных знаний и опыта.

На видео для наглядности показаны обычная и обработанная видеозаписи. Давайте посмотрим на видеоотчет о диагностике состояния насоса с помощью МА

Диагностика насоса - YouTube

Решение IRIS M не только позволило увидеть, то что кажется невидимым человеку, но и помогло определить первопричину проблем с оборудованием трещину в раме. Короткое и наглядное видео выглядит гораздо убедительнее традиционных бумажных отчетов.

Чувствительность системы 0,25 микрона с расстояния 1 метр при использовании объектива с фокусным расстоянием 50 мм. Пара человеческих глаз не выдерживает конкуренцию.

Варианты решений

Помимо первого решения IRIS M, которое закрывает основные потребности заказчиков, на рынок были выпущены решение IRIS MX в 2018г. и IRIS CM в 2019г.

IRIS M делает 120 кадров в секунду в HD-разрешении и до 1300 в сокращенном, что позволяет уверенно диагностировать проблемы в частотном диапазоне от 0 до 520 Гц.

IRIS MX расширяет возможности базового решения и позволяет работать и в более высокочастотной области до 11600 Гц (1400 fps при HD-разрешении и 29000 fps при сокращенном разрешении), что позволило успешно диагностировать турбомашины.

Решение IRIS CM (continuous monitoring) хорошо подходит для мониторинга состояния активов на удаленных объектах, на которых нет специалистов по вибродиагностике. Несколько видеокамер можно объединить в сеть, чтобы получать видеоданные с разных ракурсов. Пользователи могут инициировать запись видео и данных на основе внешних триггеров (например, данных с акселерометров) при достижения пороговых значений вибрации.

Даташиты решений можно найти по ссылке РЕШЕНИЯ VIMS (motionamplification.ru)

Интерфейсы

Пакет приложений решений RDI, установленных на ноутбуке, содержит 4 программы. По ссылкам можно увидеть их интерфейсы:

1. Motion Explorer хранение и менеджмент файлов

Управление контентом Motion Explorer - YouTube

2.RDI Acquisition запись видео

Программа для записи видео RDI Acquisition - YouTube

3. MotionAmplification аналитика и измерения

Аналитическое приложение MotionAmplification - YouTube

4. Motion Studio редактор видео

Редактор видео Motion Studio - YouTube

Как видите, для любого пользователя windows интерфейсы достаточно привычные и удобные. Даже не имея квалификацию вибродиагноста, можно стать сертифицированным оператором системы.

Возможности решения

Подробную информацию о функционале программного обеспечения и его развитии в 2016-2020 гг. можно найти по ссылке

Обзор программного обеспечения MotionAmplification v1.0-3.0 - YouTube

Диагностика насосов это типовая задача для традиционной диагностики. Но что делать, если перед нами большой резервуар, трубопровод или большая конструкция? Motion Amplification открывает специалистам по диагностике новые возможности по решению этих сложных задач.

Диагностика состояния огромных конструкций - YouTube

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

Диагностика резервуаров - YouTube

Добывающие отрасли исторически развиты в России, используется много дорогого оборудования. Давайте посмотрим на состояние этой мачты буровой установки

(1) Диагностика мачты буровой установки - YouTube

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

Вибрация трубопровода на НПЗ - YouTube

Часто важно измерить показатели вибрации во время изменения нагрузочного или скоростного режима работы оборудования. Посмотрите на изменение состояния прокатного стана на металлургическом предприятии при прокатке металла.

Диагностика состояния прокатного стана - YouTube

Теперь понятны причины быстрой деградации подшипниковых узлов.

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

Диагностика виброгрохота и насоса. Частотная фильтрация видео. - YouTube

Функциональные возможности решения быстро расширялись последние годы. Так в версии 3.0 программного обеспечения в 2020 году появились:

  • усиление движения в режиме реального времени (Live MA), которое отлично подходит для быстрого сканирования состояния активов

  • векторы движения

  • тепловая карта движения (по частотам)

  • измерения движущихся объектов

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

Заключение

Если все так хорошо, то почему эти решения не используются на каждом крупном российском предприятии? Причин тут видится несколько:

  1. запрет в США на продажу решений двойного назначения в Россию, под который до 2021г. попадали и решения Motion Amplification

  2. даже успешным на западных рынках решениям необходимо время, чтобы зарекомендовать себя на новом рынке

  3. длительные циклы принятия решений и выделения бюджетов в корпорациях

  4. в России по-прежнему доминирует планово-предупредительное обслуживание оборудования, диагностику на производствах во многих отраслях недооценивают и недофинансируют.

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

Мне представляется, что спрос на квалифицированных экспертов в узких предметных областях, таких как диагностика, будет по-прежнему высоким в ближайшие годы. Экспертно-ориентированные решения Motion Amplification существенно расширяют возможности по диагностике промышленных активов. Спектр применения и количество отраслей, использующих решения семейства IRIS, постоянно растут. С помощью этих решений процессы диагностики, технического обслуживания и ремонты на предприятиях становятся более эффективными и прозрачными.

Подробнее..

Топ-32 оптических иллюзий движения от японского художника jagarikin

30.04.2021 18:12:21 | Автор: admin
image

(1)

Есть такой японский цифровой художник (jagarikin), который экспериментриет с иллюзией движения (обратный фи-феномен), когда пиксели не двигаются, а просто меняют цвет. Его работы репостнули Илон Маск и Стивен Пинкер. За полгода я хорошенько поизучал его работы (отмотал Твиттер на 5 лет) и выбрал 32 самых крутых и залипательных. Отметьте для себя, какая иллюзия приковала ваше внимание больше всего, и поделитесь в комментариях.

Канал в Telegram Cognitive Illisions, где я буду делиться самыми вырвиглазными находками, как хакнуть мозг через восприятие.

Осторожно: гифки тяжелые!

image


(2)

Теория


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

image


Если интервал включения огоньков менее 60 мс, источники света воспринимаются как горящие одновременно.

Если интервал от 60 до 200 мс, то зажигание лампочек выглядит как непрерывное движение.

Если интервал больше 200 мс, то зритель ощущает, что лампочки загораются одна за другой.

Обратный фи-феномен это ситуация, когда иллюзия движения достигается благодаря быстрому изменению цвета и контрастности элементов изображения. Все примеры в этом посте про это.

Reverse phi illusion (wikipedia)
As apparent phi movement is perceived by humans visual system with two stationary and similar optical stimuli presented next to each other exposing successively with high frequency, there is also a reversed version of this motion, which is reversed phi illusion. Reverse phi illusion is the kind of phi phenomenon that fades or dissolves from its positive direction to the displaced negative, so that the apparent motion human perceive is opposite to the actual physical displacement. Reverse phi illusion is often followed by black and white patterns.

It is believed that reverse phi illusion is indeed brightness effects, that it occurs when brightness-reversing picture moving across our retina. It can be explained by mechanisms of visual receptive field model, where visual stimuli are summated spatially (a process that is reverse to spatial differentiation). This spacial summation blurs the contour to a small extent, and thus changes the brightness perceived. Four predictions are confirmed from this receptive field model. First, foveal reverse-phi should be broken down when the displacement is greater than the width of foveal receptive fields. Second, reverse phi illusion exists in the peripheral retina for greater displacements than in the fovea, for receptive fields are greater in the peripheral retina. Third, the spacial summation by the receptive fields could be increased by the visual blurring of the reversed phi illusion projected on a screen with defocus lens. Fourth, the amount of reversed phi illusion should be increasing with the decrease of displacement between positive and negative pictures.

Indeed, our visual system processes forward and reversed phi phenomenon in the same way. Our visual system perceives phi phenomenon between individual points of corresponding brightness in successive frames, and phi movement is determined on a local, point-for-point basis mediated by brightness instead of on a global basis.

Neural mechanism underlying sensitivity to reversed phi phenomenon
  • T4 and T5 motion detectors cells are necessary and sufficient for reversed phi behavior, and there is no other pathways to produce turning responses for reversed phi motion
  • Tangential cells show partial voltage response with the stimulation of reversed phi motion
  • Hassenstein-Reichardt detector model
  • There is substantial responses for reversed-phi in T4 dendrites, and marginal responses in T5 dendrites



image

(3)

Иллюзия достигается за счет колец 1-пиксельной толщины по периметру основных колец.
При рассинхронизации вращения тонких колец относительно основных и достигается такой эффект.

image


Предыдущую итерацию обсудили на Хабре в прошлом году. Там есть наглядные описания механизма иллюзии.

Стивен Пинкер восхитился работой художника:

image

(4)

И объяснил ему, как работает эта иллюзия:

image

image


image

(5)

image

(6)

image

(7)

image

(8)

image

(9)

image

(10)

image

(11)

image

(12)

image

(13)

image

(14)

image

(15)

image

(16)

image

(17)

image

(18)

image

(19)

image

(20)

image

(21)

image

(22)

image

(23)

image

(24)

image

(25)

image

(26)

Рекламный потенциал


image

(27)

image

(28)

image

(29)

image

(30)

image

(31)

image

(32)



Канал в Telegram Cognitive Illisions, где я буду делиться самыми вырвиглазными находками, как хакнуть мозг через восприятие.

Читать еще


Подробнее..

Перевод Полив газона с помощью модели сегментации изображений и системы на базе Arduino

30.04.2021 20:15:00 | Автор: admin

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


Задача

Представьте, что вы прогуливаетесь по своему кварталу мимо красивых зелёных лужаек. Что такое?.. Вода же должна литься на газон, а не на тротуар рядом! Здесь люди ходят! Слева от вас большой газон орошается из-под земли десятком спринклеров. Но, хоть вся трава и поливается обильно, на газоне тут и там заметны проплешины. Многие не видят в этом проблемы эка невидаль! и безмятежно прыгают через лужи. Но проблема здесь не только в лужах, а в том, что, несмотря на использование такого количества воды, газон всё равно не растёт нормально. И проблема эта более серьёзная, чем можно подумать. В Америке от 30 до 60 % городской пресной воды используется для полива газонов, и самое печальное, что приблизительно 50 % этой воды тратится впустую из-за небрежно или неправильно установленной системы полива.

Уход за газонами недешёвое занятие. Чтобы газоны росли так, как нужно, необходимо правильно подобрать удобрения, компост и, самое главное, правильно их поливать. В этой статье я расскажу о разработанном мною решении какие шаги я предпринял при планировании, создании прототипа и внедрении.

Первоначальные соображения

В начале этого проекта у меня не было ни опыта работы с методами машинного распознавания образов, не говоря уже о каком-никаком плане.

Сухие проплешины.Сухие проплешины.

Разница в цвете между светло-коричневыми участками и зеленью здоровой травы сразу бросается в глаза. Я начал решать задачу так: проанализировал RGB-значения различных областей изображения. Если бы я мог выявлять "менее зелёные" области и найти критерий, по которому такие участки можно было выделять, я мог бы их точно определить. Однако всё оказалось не так просто, как я думал.

Главной проблемой было множество внешних факторов, на которые я повлиять не мог, но они могли сами повлиять на результаты. Например, RGB-значение участка изображения может быть изменено ночным электрическим освещением. При таком количестве посторонних факторов система была бы просто нежизнеспособна. Мне определённо нужно было искать лучшее решение.

Сегментация изображений

Новое решение оформилось в виде концепции, называемой сегментацией изображения. Сегментация изображений это процесс машинного распознавания образов, разделяющий изображения на "сегменты" на основе сходства пикселей. В основном он используется, чтобы обнаруживать на изображении объекты (в таком случае он называется методом обнаружения объектов), и применяется в автономных транспортных средствах, при обработке медицинских изображений и спутниковой съёмке.

Сегментация изображений.Сегментация изображений.

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

Я обнаружил замечательную библиотеку под названием ImageAI, написанную Олафенвой Мозесом. Библиотека ImageAI позволяет обнаруживать объекты с использованием набора данных, созданного пользователем. С помощью ImageAI мне удалось подобрать оптимальный набор данных, наиболее подходящий для моей модели.

Первый набор данных / Тестовый

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

Бесплатные общедоступные наборы данных можно найти на множестве веб-сайтов и во многих приложениях. Эти данные, в частности, могут использовать инженеры при работе над проектами. Однако для моего проекта никаких наборов данных не было, и поэтому мне пришлось создавать свои собственные. Как выяснилось, создание набора данных оказалось чуть ли не самой сложной проблемой во всём проекте.

Набор данных для сегментации изображений состоит из двух частей: изображений и аннотаций. Существует множество способов аннотирования изображений, то есть пометок места расположения объекта на изображении. Я использовал формат Pascal VOC, сохраняющий аннотации в файлах .xml. То есть, если мой набор данных содержит 50 изображений, мне пришлось бы аннотировать каждое отдельное изображение и создать 50 xml-файлов с аннотациями соответствующих изображений.

Вначале у меня было много вопросов о наборе данных: сколько изображений нужно иметь, как аннотировать изображение, как использовать аннотацию в коде, как узнать, подходит ли для модели то или иное изображение, и так далее. Тут мне здорово помог Олафенва Мозес, который, к счастью, написал очень информативную статью о том, как создавать пользовательский набор данных и использовать его в библиотеке ImageAI.

Первым шагом было получение изображений, и он оказался значительно сложнее, чем предполагалось. Как я ни старался, я не смог найти в сети нужные мне высококачественные изображения. Я погуглил строку "трава с проплешинами", и для первого набора данных мне удалось загрузить всего 65 изображений. Чтобы вы понимали большинство наборов данных содержат тысячи изображений, и только тогда их имеет смысл использовать для обучения модели.

После сбора изображений я закомментировал их с помощью программы Label IMG. Я создал каталог с изображениями и каталог для аннотаций. Затем я поставил аннотацию к каждому изображению, обведя в рамки все проплешины. Привожу ниже скриншот программы Label IMG, которую я использовал для аннотирования изображений.

Программа Label IMG.Программа Label IMG.

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

Набор данных 1. Результаты.Набор данных 1. Результаты.

После тестирования модели на изображении моей лужайки перед домом у меня получился вот такой результат. Я так долго не мог добиться, чтобы программа хотя бы доходила до конца без ошибок, что был в восторге даже от того, что она, наконец, обвела проплешину рамкой! То, что полученный результат был ужасен и абсолютно бесполезен для работы системы, я понял уже позднее. Модель, в принципе, способна обозначать область с плохой травой. Однако получившиеся границы очень неточны, и такой результат практически бесполезен.

Наборы данных 24 / Последующие тесты

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

Полистав Интернет, я обнаружил такую вещь, как парсеры (web scrapers). Парсер это инструмент, способный извлекать данные и содержимое с веб-сайтов и загружать эти файлы на локальный компьютер. Это было как раз то, что мне нужно, и после изучения краткого руководства я создал элементарный парсер, загружающий изображения, содержащие ключевые слова "трава с проплешинами", "плохая трава" и "плохой газон". С помощью этого парсера я собрал папку из 180 изображений, и это был мой второй набор данных.

После удаления всех непригодных изображений у меня получился третий набор данных из 160 изображений. После обучения модели на третьем наборе данных результаты всё равно оставались нестабильными и неточными. Как и в прошлый раз, я подумал, что главная проблема заключается в отсутствии достаточного количества изображений в наборе.

Я обратился кое к кому за помощью, поизучал ещё теорию и вычитал, что есть такой хитрый приём аугментация изображений (image augmentation), то есть процесс, с помощью которого набор данных может быть расширен посредством внесения изменений в существующие изображения. Количество изображений меняется за счёт того, что меняются их ориентация и RGB-значения пикселей. Пример аугментации изображений.

Пример аугментации изображений.Пример аугментации изображений.Пример дополненных и реальных данных.Пример дополненных и реальных данных.

Для нас, людей, все приведённые выше изображения одинаковы, если не считать крохотных изменений; однако для компьютера, который рассматривает изображения как массивы значений пикселей, эти изображения совершенно разные. Попробуем использовать эту идею и создадим больше обучающих данных. И вот, я, страдавший от нехватки изображений, сразу получил их много больше, чем значительно улучшил свой набор данных. Для пополнения всего каталога изображений я использовал библиотеку Keras с определёнными параметрами и граничными значениями. Мой четвёртый набор данных содержал 300 изображений. Теперь я был уверен, что модель, наконец, заработает. Но неожиданно возникла ещё одна серьёзная проблема.

Проблема совместимости библиотек

В большинстве проектов по программированию, особенно проектов в области анализа и обработки данных, для работы определённых функций и инструментов требуется ряд библиотек и зависимостей. В этом конкретном проекте библиотека ImageAI потребовала установки определённых версий различных библиотек, в том числе tensorflow-gpu 1.13 и keras 2.4.

Разные версии библиотеки отличаются одна от другой, причём использование разных версий может повлиять на взаимодействие между библиотеками. В моём случае это было особенно актуально, так как обучение и работа модели были возможны только при использовании определённых версий библиотек.

В январе вышло обновление библиотеки ImageAI, и оно сразу поставило крест на работе других библиотек, которые я использовал в проекте, оно было просто несовместимо с ними. И вот, время обучения, обычно составлявшее около 5 минут на эпоху, стало составлять более 14 часов. Кроме того, модель постоянно перестраивалась под данные, а это свидетельствовало о том, что она была неспособна генерализовывать новые данные.

Сначала я подумал, что такое длительное время обучения обусловлено большим количеством изображений в наборе данных, но скоро стало очевидно, что проблема кроется не в этом и она более серьёзная. Я ещё раз обратился к учебникам, но ничего путного найти не смог. Если бы вы знали, сколько различных вариантов я перепробовал, чтобы устранить проблему!.. Я пробовал менять способы извлечения набора данных, менять аппаратный ускоритель, менять размер пакета... Ни один из этих вариантов не снизил время обучения модели.

Но тут я наткнулся на недавно опубликованный пост в разделе проблем и вопросов на Github, в котором кто-то жаловался на такую же точно проблему, как и у меня. Олафенва Мозес, создатель библиотеки, ответил на это пост и объяснил проблему, предложив собственное решение. Суть этого решения была такой: три основные библиотеки Tensorflow, Keras и ImageAI должны иметь чётко определённые версии.

Окончательный набор данных / Модель

После избавления от предыдущей проблемы я сначала протестировал набор данных из 300 изображений. Результаты улучшились, но ненамного, им не хватало стабильности. На некоторых изображениях газона с разных углов моя модель вообще не смогла обнаружить проплешин.

Набор данных 4. Результаты.Набор данных 4. Результаты.

Я решил добавить ещё больше данных и в итоге получил набор из 1738 изображений. Чтобы обучить модель, мне пришлось аннотировать каждое изображение, вручную очерчивая участки на каждом из них. К счастью, этой работой я занимался не один, а с друзьями. Менее чем за два часа нам удалось аннотировать все изображения.

Окончательный набор данных был разделён на 1400 тренировочных и 338 тестовых изображений. После обучения модели за 5 эпох я провел валидацию и получил впечатляющий результат 0,7204, что, безусловно, стало моим лучшим результатом с начала проекта.

Набор данных 5. Результаты.Набор данных 5. Результаты.

Окончательный результат работы модели меня, наконец, удовлетворил. После обучения модели я приступил к разработке прототипа системы, которая могла бы целенаправленно поливать участки газона.

Создание спринклера

Схема системы.Схема системы.

Чтобы контролировать полив, мне нужно было обеспечить вращение спринклера по двум осям так я мог бы контролировать расстояние и направление разбрызгивания воды. Я использовал два шаговых двигателя NEMA с разными характеристиками мощности и крутящего момента. Нижний двигатель NEMA-23 использовался для управления направлением разбрызгивания воды. Верхний двигатель NEMA 14 вращал стержень с закреплённым на нём с помощью трубки из ПВХ спринклером, чтобы можно было управлять расстоянием, на которое разбрызгивается вода. Для управления этими двигателями я использовал Arduino, два регулятора частоты вращения двигателя A4988 и два адаптера питания 12 В.

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

Я рассчитал угол, на который необходимо повернуть спринклер от начального места до центра проплешины, определил приблизительное количество пикселей/футов на моём изображении и после этого вычислил приблизительное расстояние до центра проплешины.

Результат работы скрипта управления двигателем.Результат работы скрипта управления двигателем.

На рисунке выше показаны результаты работы скрипта управления двигателем. Их них следует, что спринклер должен повернуться на 42, а расстояние до проплешины составляет примерно 3 м 89 см.

Согласно скрипту угол до проплешины составляет 42.Согласно скрипту угол до проплешины составляет 42.

Длина рулетки 3 м (10 футов); от центра проплешины до спринклера примерно 12 футов 9 дюймов (3,8862 м), как и предсказал скрипт.

Я выполнил несколько тестов для определения коэффициентов преобразования шагов в углы и шагов в футы, и это позволило мне создать скрипт Arduino для ориентирования спринклера на проплешину с использованием данных скрипта управления двигателем.

Готовность к окончательному тестированию

К сожалению, всю систему в итоге мне протестировать не удалось.

Перед окончательным тестом я стал проверять готовую конструкцию, чтобы убедиться, что она способна держать спринклер и поворачивать его. Но я был очень неосторожен: в моторы попала вода, и они сгорели. Двигатели были довольно дорогими, а покупать новые было непрактично.

Анализ кода / Краткий обзор

Google Colab PatchDetector

PatchDetector на Github

!wget https://github.com/fazalmittu/PatchDetection/raw/master/BlackedOutLawn.jpg!wget https://github.com/fazalmittu/PatchDetection/raw/master/BlackedOutLawn_Detected.jpg!wget https://github.com/fazalmittu/PatchDetection/releases/download/v3.0/detection_model-ex-04--loss-25.86.h5!wget https://github.com/fazalmittu/PatchDetection/releases/download/v3.0/detection_config1700_v1.json!wget https://github.com/fazalmittu/PatchDetection/raw/master/BlackedOutFullLawn.jpg!wget https://github.com/fazalmittu/PatchDetection/raw/master/BlackedOutFullLawn_Detected.jpg!wget https://github.com/fazalmittu/PatchDetection/raw/master/SprinklerPOV.jpg!wget https://github.com/fazalmittu/PatchDetection/raw/master/SprinklerPOV_Detected.jpg!wget https://github.com/OlafenwaMoses/ImageAI/releases/download/essential-v4/pretrained-yolov3.h5

Этот код используется для импорта модели и изображений (для тестирования), которые я хранил на Github, чтобы их можно было легко извлечь с помощью Google Colab. Последняя строка импортирование предварительно обученной модели YOLO-v3, которая, в свою очередь, использовалась для обучения модели (трансферное обучение).

!pip uninstall -y tensorflow!pip install tensorflow-gpu==1.13.1!pip install keras==2.2.4!pip install imageai==2.1.0!pip install numpy

Этот код импортирует определённые версии библиотек, необходимых при проектировании. Используемые библиотеки: tensorflow-gpu, keras, imageai и numpy.

%load_ext autoreload%autoreload 2  from google.colab import driveimport sysfrom pathlib import Pathdrive.mount("/content/drive", force_remount=True)base = Path('/content/drive/MyDrive/PatchDetectorProject/')sys.path.append(str(base))zip_path = base/'AugmentedDataSetFinal.zip'!cp "{zip_path}" .!unzip -q AugmentedDataSetFinal.zip!rm AugmentedDataSetFinal.zip

Этот код используется для получения набора данных из места, которое я определил для него на Google-диске. Для сохранения на Github набор данных был слишком велик, поэтому мне пришлось использовать альтернативный источник.

from imageai.Detection.Custom import DetectionModelTrainerfrom __future__ import print_functiontrainer = DetectionModelTrainer()trainer.setModelTypeAsYOLOv3()trainer.setDataDirectory(data_directory="AugmentedDataSetFinal")trainer.setTrainConfig(object_names_array=["patch"], batch_size=4, num_experiments=5, train_from_pretrained_model="pretrained-yolov3.h5")trainer.trainModel()

В этом коде осуществляется обучение модели. В нём указывается объект для поиска ("patch" (проплешина)), количество эпох (5), размер пакета (4) и используется трансферное обучение.

from imageai.Detection.Custom import DetectionModelTrainertrainer = DetectionModelTrainer()trainer.setModelTypeAsYOLOv3()trainer.setDataDirectory(data_directory="AugmentedDataSetFinal")trainer.evaluateModel(model_path="AugmentedDataSetFinal/models", json_path="AugmentedDataSetFinal/json/detection_config.json", iou_threshold=0.5, object_threshold=0.3, nms_threshold=0.5)

Этот код используется для валидации модели. С окончательной моделью я получил оценку 72,04 %. Я считаю этот результат очень хорошим, учитывая, что обнаруживаемые мною объекты представляют собой проплешины без определённой формы, цвета или размера.

from imageai.Detection.Custom import CustomObjectDetectionfrom PIL import Image, ImageDrawimport numpy as npdetector = CustomObjectDetection()detector.setModelTypeAsYOLOv3()detector.setModelPath("/content/detection_model-ex-04--loss-25.86.h5")detector.setJsonPath("/content/detection_config1700_v1.json")detector.loadModel()detections = detector.detectObjectsFromImage(input_image="SprinklerPOV.jpg", output_image_path="SprinklerPOV_Detected.jpg", minimum_percentage_probability=30) i = 0coord_array = []for detection in detections:    coord_array.append(detection["box_points"])    print(detection["name"], " : ", detection["percentage_probability"], " : ", detection["box_points"])    i+=1print(coord_array)detected = Image.open("SprinklerPOV_Detected.jpg")box = ImageDraw.Draw(detected)for i in range(len(coord_array)):  box.rectangle(coord_array[i], width=10)detected

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

!wget https://github.com/fazalmittu/PatchDetection/raw/master/FeetToPixel.JPGimg_ft = Image.open("FeetToPixel.JPG")ft_line = ImageDraw.Draw(img_ft)ft_line.line([(175, 1351), (362, 1360)], fill=(0, 255, 0), width=10)ft_distance = np.sqrt(9*9 + 187*187)print(ft_distance)img_ft

С этого кода начинается скрипт управления двигателем. Код импортирует изображение, на нём разложенная на траве рулетка. Я использовал это изображение для определения примерного количества пикселей в футе.

Пиксели/футы.Пиксели/футы.
from PIL import Image, ImageDraw#TOP LEFT = [0, 1]#BOTTOM LEFT = [0, 3]#TOP RIGHT = [2, 1]#BOTTOM RIGHT = [2, 3]img = Image.open("SprinklerPOV_Detected.jpg")middle_line = ImageDraw.Draw(img)avg_1Line = ImageDraw.Draw(img)avg_2Line = ImageDraw.Draw(img)avg_1 = (coord_array[1][1] + coord_array[1][3])/2avg_2 = (coord_array[1][0] + coord_array[1][2])/2middle_line.line([(2180, 0), (2180, 3024)], fill=(0, 255, 0), width=10)# avg_1Line.line([(coord_array[1][0], coord_array[1][1]), (coord_array[1][0], coord_array[1][3])], fill=(255, 0, 0), width=10)# avg_2Line.line([(coord_array[1][0], coord_array[1][3]), (coord_array[1][2], coord_array[1][3])], fill=(255, 0, 0), width=10)def find_angle():  line_to_patch = ImageDraw.Draw(img)  line_to_patch.line([(avg_2, avg_1), (2180, 3024)], fill=(255, 0, 0), width=10)  length_1_vertical = 3024 - avg_1  length_2_horizontal = 2500 - avg_2  print("Distance = ", np.sqrt(length_1_vertical*length_1_vertical + length_2_horizontal*length_2_horizontal)/ft_distance, "ft")  angle_radians = np.arctan(length_2_horizontal/length_1_vertical)  angle = (180/(np.pi/angle_radians)) #Convert radians to degrees  return angleprint(avg_1, avg_2)print(find_angle())img

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

/* *  * Fazal Mittu; Sprinkler Control *  */const int ROTATEstepPin = 3; const int ROTATEdirPin = 4; const int ANGLEstepPin = 6;const int ANGLEdirPin = 7;const int ROTATEangle = 42.25191181;//TODO: Find Conversion: Steps --> Angle 1000 steps = 90 degreesconst int ANGLEangle = 12.76187539;// pixels --> ft: 187 pixels = 1 feetbool TURN = true;float angleToSteps(float angle){  float steps = 1000/(90/angle);  return steps;}float ftToSteps(float feet) {  float steps = 100/(8/feet);  return steps;}void setup() {  Serial.begin(9600);  pinMode(ROTATEstepPin,OUTPUT);   pinMode(ROTATEdirPin,OUTPUT);  pinMode(ANGLEstepPin,OUTPUT);   pinMode(ANGLEdirPin,OUTPUT);}void loop() {  int ROTATEsteps = angleToSteps(ROTATEangle); //Angle was determined using Python Script  int ANGLEsteps = angleToSteps(ANGLEangle);  delay(7000);  if (TURN == true) {    for(int x = 0; x < ROTATEsteps; x++) {      digitalWrite(ROTATEstepPin,HIGH);       delayMicroseconds(500);       digitalWrite(ROTATEstepPin,LOW);       delayMicroseconds(500);     }    delay(5000);    for (int x = 0; x < 100; x++) { //100 steps = 8 ft, 0 steps = 14.5 ft      digitalWrite(ANGLEstepPin,HIGH);       delayMicroseconds(500);       digitalWrite(ANGLEstepPin,LOW);       delayMicroseconds(500);    }  }  TURN = false;}

Приведённый выше код использовался для управления обоими двигателями. Перед использованием любого кода сначала нужно определить взаимозависимости шаги/градусы и шаги/футы, что я и сделал посредством проведения тестов и измерений. В итоге у меня появился рабочий скрипт, который мог управлять работой двигателей в соответствии со значениями, полученными в результате работы скрипта управления.

Заключение

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

А если вы хотите научиться работать с данными и обрабатывать их помощью машинного обучения обратите внимание на наш курс по ML или на его расширенную версию Machine Learning и Deep Learning, партнером которого является компания Nvidia.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Прямо как в Матрице нейросеть обучили изменять ракурс любых видео

21.05.2021 16:09:34 | Автор: admin
В фильмах или роликах с YouTube мы наблюдаем происходящее из одной точки, нам не доступны перемещение по сцене или смещение угла зрения. Но, кажется, ситуация меняется. Так, исследователи из Политехнического университета Вирджинии и Facebook разработали новый алгоритм обработки видео. Благодаря ему, можно произвольно изменять угол просмотра уже готового видеопотока. Что примечательно алгоритм использует кадры, которые получены при съемке на одну камеру, совмещение нескольких видеопотоков с разных камер не требуется.

В основе нового алгоритма нейросеть NeRF (Neural Radiance Fields for Unconstrained). Эта появившаяся в прошлом году сеть умеет превращать фотографии в объемную анимацию. Однако для достижения эффекта перемещения в видео проект пришлось существенно доработать.

Что именно умеет NeRF?



NeRF обучают на большом количестве изображений одного и того же объекта с разных ракурсов. Изначально ее научили воссоздавать объемные картинки известных архитектурных достопримечательностей и других объектов. Не все изображения идеально складывались и читались, поэтому требовалось расширить изначальные возможности.


Сама по себе нейросеть умеет создавать 3D-изображение под разными углами из множества снимков. Также она может вычленять 2D-модели. Эти изображения переводят из объемных в плоскостные путем попиксельного переноса. Как именно?

Когда выбран нужный угол зрения, алгоритм начинает пропускать сквозь трехмерную сцену лучи и попиксельно создавать двухмерное изображение, используя данные о плотности и цвете каждого пикселя вдоль всего прохождения луча. Чтобы получить кадр целиком, процесс запускают несколько раз, пока не наберется необходимое количество пикселей из лучей просчитанных с разных направлений. Тем самым становится возможным генерация 2D-изображений с любого ракурса. В целом интересный и оригинальный подход к решению столь нетривиальной задачи.

Видеопоток с эффектом


Для достижения похожего эффекта для видеопотока требуются аналогичные действия. Но есть важный нюанс в видео есть только один ракурс для каждого отдельного кадра. Чтобы справиться с задачей, исследователи решили обучить две разные модели параллельно. В итоге получились сразу 2 алгоритма: для статической и динамической частей сцены.


Что касается статичной модели, то она устроена по тому же принципу, что и NeRF. Есть только одно отличие из кадра сразу удалили все движущиеся объекты.


С динамической моделью все намного интереснее. Для ее обработки не хватало кадров. Тогда нейросеть научили предсказывать кадры для объемного потока. Точнее кадры к каждому конкретному моменту времени t. Эти моменты условно назвали t-1 и t+1. Суть 3D-потока сводится к оптическому потоку, только в этом случае его строят для объемных объектов.

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

Сами разработчики считают свой проект более удачной версией NeRF, благодаря стройным и плавным переходам.

Подробнее..

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

Я перепробовал все решения, что нашёл в интернете и они оказались неактуальными - раз, два, три, официальное руководство и видео есть. Поэтому будем собирать пакет вручную.

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

Если вы посмотрели видео, то поняли, что результат ещё далёк от реальной эксплуатации.

Признаю честно - до этого момента никогда не сталкивался с многопоточностью на Python, поэтому "с наскоку" тему взять не удалось и результат по видео понятен. Есть неплохая статья на Хабре, описывающая различные методы многопоточности, применяемые в языке. Пока у меня решения нету по этой теме нету - будет повод разобраться лучше и дописать код/статью с учетом этого.

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

Другая интересная особенность - таймеры в Питоне. Я, опять же, признаю, что ни разу до этого не было нужды в них, но все статьях сводится к тому, чтобы ставить поток в sleep(кол-во секунд). А если мне нужно сделать так, чтобы основной поток был в работе, а по истечению n-ое количества секунд не было активности, то выполнялась моя функция? Использовать демонов (daemon)? Так это не совсем то, что нужно. Писать отдельную программу, которая взаимодействует с другой? Возможно, но единство программы пропадает.

Заключение

На своём примере могу точно сказать - не все руководства одинаково полезны и простая задача может перерасти в сложную. Я понимаю, что большинство материалов является переводом/простым копированием документации. Но если ты пишешь руководство, то проверь хотя бы на тестовой системе, что написанные тобой действия точно повторимы. Пробуйте, экспериментируйте - это хороший повод изучить и узнать новое.

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Перевод Улучшение улучшенного фотореализма

14.05.2021 18:17:37 | Автор: admin
Разработчики из Intel Labs при помощи сверточной нейросети улучшают синтертические изображения, повышаеют их стабильность и реализм.

GTA V to Cityscapes


Модификации, выполненные нашим методом, геометрически и семантически согласуются с исходными изображениями.

image

Модификации также стабильны во времени:



Озеленяем выжженную траву и холмы в Калифорнии в GTA:

image

Добавляем отражения в окна и увеличиваем эффект Френеля (например, на крыше автомобилей):

image

Восстанавливаем дороги:

image

image

image

image

Перевод GTA V в Mapillary Vistas


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

image

Удаляем далекую дымку и перестраиваем дорогу:

image

Траву делаем более объемной и зеленой:

image

image

image

image

Подробнее..

Штрих-код

16.05.2021 12:20:18 | Автор: admin
image

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

Сама идея создания универсального кода, в котором была бы зашифрована информация о товаре, была подслушана товарищем Вудленда, Бернаром Сильвером, причем подслушана буквально: он стал свидетелем разговора своего декана (Вудленд и Сильвер учились тогда в аспирантуре) с директором супермаркета, который спрашивал, способны ли ученые создать систему, позволяющую мгновенно регистрировать покупку и вести учет товара.
Декан от такой задачи отказался, а Вудленд и Сильвер буквально загорелись этой идеей: она показалась им вполне решаемой, а её будущее великолепным.

image

Супермаркет в США, 60-е г.г. Любой торговец скажет, что скорость оборота один из самых главных факторов успеха в торговле, а в розничной торговле на скорость оборота влияют быстрота заказа реализованного, быстрота выкладки товара и быстрота обработки покупок

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

Впрочем, в нашу страну шрих-коды попали совсем недавно, и, заставшим советскую торговлю людям можно и не рассказывать, как работала розница без шрих-кода: вот только американский коммерчески ориентированный ритейл не мог себе позволить, как советская торговля, периодически закрываться на учёты и переучёты.

Вудленд и Сильвер, впрочем, быстро убедились, что их декан был прав: задача оказалась совсем не банальной.
Но задача уже полностью захватила молодых людей. В 1948-м Вудленд прерывает свою учебу в аспирантуре, которая мешает ему сосредоточится на решении целиком, и уезжает в Майами, к своему деду.
И там к нему в самом деле приходит решение: он вспоминает вдруг, как в свои бойскаутские времена он изучал азбуку Морзе. Песок пляжа, на котором он пробует изобразить точки и тире, наталкивает его на мысль о передаче содержания с помощью линий различной толщины.
Кодировку надо было как-то считывать, и Вудленд и Сильвер придумали использовать для этого технологию передачи звука в кино, придуманную за два десятка лет до них Лу де Форестом, которая заключалась в просвечивании разной интенсивности цвета, наносимого по краю кинопленки.
Уже в 1949 году друзья получили патент на свое изобретение (их код выглядел как яблоко, полоски расположены были по кругу), и

image

Одна из первых разработок Вудленда и Сильвера. Штрих-код здесь располагается по кругу, в этом авторы видели определенный плюс. Появившийся позже сканер изменил расположение полосок

Нет, мир не стал мгновенно счастливее: на этапе конструирования, уже работая в IBM, куда их пригласили как авторов приглянувшейся руководству компании перспективной идеи, изобретатели столкнулись с проблемой считывания кода, а точнее с проблемой создания адекватного устройства.
Их первый сканер представлял из себя устройство размером со стол, крышка которого была прозрачной, а под крышкой были мощные лампы. Получаемый сигнал передавался на осциллограф, который должен был фиксировать результат.
Первые испытания закончились пожаром: лампы раскалили крышку, бумага задымилась, но, тем не менее, первый результат был получен.
Увы, он не удовлетворил ни самих изобретателей, ни торговлю.

Прекрасная идея, как оказалось, несколько опередила время: проблема считывания кода выглядела нерешаемой, и тем более не решаемой была проблема обработки полученной информации. Эта задача возлагалась на ЭВМ, но ЭВМ тех лет были чрезвычайно громоздки и еще только учились решать задачи сложнее арифметических действий.

Казалось, на коммерческой реализации проекта был поставлен крест, в IBM официально закрыли проект, Вудленд и Сильвер продают свой патент в компанию RCA за скромные $15000.
RCA вцепляется в идею шрих-кода, и много лет экспериментирует с ней, пробуя заинтересовать им торговлю.
Они первые догадались применить для сканирования кода лазер: тонкий гелиево-неоный луч прекрасно подходил для распознавания кода.
У компании есть и первые, пусть и скромные, успехи: их кодом начинают (пусть и не без проблем) пользоваться при железнодорожных и морских перевозках.
В конце концов, их активность оказывается замеченной: в начале 70-х американский союз супермаркетов объявляет конкурс на лучший код.

image

Норман Вудленд, Джодж Лорер и Бернар Сильвер, создатели штрих-кода

Корпорация IBM мгновенно возвращается в игру задача кажется чрезвычайно привлекательной для использования потенциала компании.
Создается рабочая группа по руководством отличного инженера и прекрасного руководителя Джорджа Лорера. Вместе с математиком Дэвидом Савиром они приступают к решению.
Тут кто-то из ветеранов IBM вспоминает про двух талантливых ребят, занимавшихся у них этой проблемой в 50-е.
Сильвера к тому времени уже нет в живых, но Вудленд не растерял своих способностей, и он с азартом включается в работу.

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

В итоге именно простое и элегантное решение от IBM пришлось по вкусу американской торговле, и 3 апреля 1973 года считается рождением UPC (Universal Product Code).
В 1974 году состоялась первая продажа товара с отсканированным штрих-кодом.
Американская розница тут же предложила своим поставщикам наносить штрих-коды на упаковку, а производители скоро и сами оценили удобство такой маркировки, которая позволяла им четко отслеживать отгрузку и движение товара.

Как и все, пусть даже самые прекрасные изобретения, штрих-код распространяется не мгновенно: еще в 1979 году всего лишь 1% американских продавцов пользуется этой технологией, но уже в начале 80-х число пользователей превышает 90%, а к середине того же десятилетия в США уже не остается торговых точек, которые не использовали бы штрихкодирование.

image

США, середина 70-х, сканирование покупок на кассе. Надо сказать, в те годы это всё ещё большая экзотика

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

Необходимость стандартизации приводит с созданию специального органа по предоставлению и классификации штрих-кода UCC.

image

Первый в мире продукт со штрих-кодом

Говорят (чего только не расскажут!), что жена Лорера каждый раз, приходя в магазин, с гордостью говорила кассирам и покупателям: Это штрих-код, его придумал мой муж!. Её, кажется, забавляло недоумение буквально всех, кому она говорила об этом мол, а что, когда-то были времена, когда штрих-кода не существовало?

Разумеется, такие времена были, и, может быть, кто-то из нас их даже сможет вспомнить: в СССР первая покупка товара со штрих-кодом состоялась в 1990 году, а сам штрих-код стал заметным (но даже тогда еще не всеобщим) явлением в нашей стране только к XXI веку.

В настоящее время у него появляются конкуренты: RFID, QR и несколько менее известные форматы.
Но при всем этом позиции штрих-кода выглядят незыблемыми: сегодня каждый день в мире сканируется более 6 млрд. штрих-кодов.

Александр Иванов, специально для блога VDSina



На правах рекламы


Воплощайте любые идеи и проекты с помощью наших VDS Windows или Linux. Сервер готов к работе через минуту после оплаты!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Самые креативные капчи DOOM, приседания, ползунки, резисторы, матан

26.05.2021 02:06:39 | Автор: admin
Своими действиями или бездействием нанесите вред человеку, чтобы доказать, что вы не робот.
капча по Азимову

Капча с DOOM уже несколько дней одна из самых обсуждаемых тем на Reddit и HackerNews. А какие еще бывают креативные капчи?

Doom Captcha


image


Шуточная капча, в которой пользователю необходимо сыграть в мини-версию Doom для доказательства того, что он не робот. Её создал программист Мигель Ортеза, а ознакомиться с ней можно на GitHub.

IDDQD тоже работает.


Squat Captcha




Самая отвратительная капча на свете. Заставляет сделать 10 приседаний, защищая вас от спонтанных покупок. чтобы продолжить. Работает с Chrome/Firefox на десктопе, требуется веб-камера.

Подробности тут.

Motion Captcha


image

Вам нужно нарисовать предложенную фигуру.

GitHub

They Make Apps


image

Специальный слайдер, который нужно немного покрутить, чтобы заполнить форму. Задача звучит примерно так: Докажите что вы человек; Расположите курсор в конце линии, чтобы зарегистрироваться на нашем сайте.

Resistor CAPTCHA


image

Матановая капча


image



Chesscaptcha

(по наводке grayfolk)

image

Нужно либо расположить фигурки правильно, либо сделать мат в один ход.

Подробнее тут.

Ёще идеи для капчи


image

Капча на возраст.

image

Игровые капчи.

image

Капчи, в которых надо перетаскивать предметы.

Подробнее..

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

11.06.2021 08:06:47 | Автор: admin

Фиксация различных нарушений, контроль доступа, розыск и отслеживание автомобилей лишь часть задач, для которых требуется по фотографии определить номер автомобиля (государственный регистрационный знак или ГРЗ).

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

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных голов. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита 1234567890ABEKMHOPCTYX, плюс специальный символ - (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча 128. Длительность обучения установим в 10 эпох.

В качестве предобработки входных изображений будем выполнять нормализацию и преобразование к единому размеру.

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):   def __init__(self, data_path, transform, label_encoder):       super().__init__()       self.data_path = data_path       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))       self.transform = transform       self.label_encoder = label_encoder    def __len__(self):       return len(self.image_fnames)    def __getitem__(self, idx):       img_fname = self.image_fnames[idx]       img = cv2.imread(img_fname)       if self.transform:           transformed = self.transform(image=img)           img = transformed["image"]       img = img.transpose(2, 0, 1)             label_fname = os.path.join(self.data_path, "ann",                                  os.path.basename(img_fname).replace(".png", ".json"))       with open(label_fname, "rt") as label_file:           label_struct = json.load(label_file)           label = label_struct["description"]       label = self.label_encoder.encode(label)        return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):       super().__init__()        self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)        self.heads = nn.ModuleList([           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)       ])     def forward(self, x):       features = self.backbone(x)       logits = [head(features) for head in self.heads]       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

Метод handle_batch, как следует из названия, отвечает за обработку батча данных. Мы в нём будем только вызывать модель с данными батча, а обработку полученных результатов расчёт лосса, метрик и т.д. мы реализуем с помощью колбэков. Код метода представлен в листинге 3.

class MultiheadClassificationRunner(dl.Runner):   def __init__(self, num_heads, *args, **kwargs):       super().__init__(*args, **kwargs)       self.num_heads = num_heads    def handle_batch(self, batch):       x, targets = batch       logits = self.model(x)             batch_dict = { "features": x }       for i in range(self.num_heads):           batch_dict[f"targets{i}"] = targets[i]       for i in range(self.num_heads):           batch_dict[f"logits{i}"] = logits[i]             self.batch = batch_dict

Листинг 3. Реализация runnerа

Колбэки мы будем использовать следующие:

  • CriterionCallback для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback для агрегации лоссов отдельных голов в единый лосс модели.

  • OptimizerCallback чтобы запускать оптимизатор и обновлять веса модели.

  • SchedulerCallback для запуска LR Schedulerа.

  • AccuracyCallback чтобы иметь представление о точности классификации каждой из голов в ходе обучения модели.

  • CheckpointCallback чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):   cbs = [       *[           dl.CriterionCallback(               metric_key=f"loss{i}",               input_key=f"logits{i}",               target_key=f"targets{i}"           )           for i in range(num_heads)       ],       dl.MetricAggregationCallback(           metric_key="loss",           metrics=[f"loss{i}" for i in range(num_heads)],           mode="mean"       ),       dl.OptimizerCallback(metric_key="loss"),       dl.SchedulerCallback(),       *[           dl.AccuracyCallback(               input_key=f"logits{i}",               target_key=f"targets{i}",               num_classes=num_classes_per_head,               suffix=f"{i}"           )           for i in range(num_heads)       ],       dl.CheckpointCallback(           logdir=os.path.join(logdir, "checkpoints"),           loader_key="valid",           metric_key="loss",           minimize=True,           save_n_best=1       )   ]     return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия train loss, синяя valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

Здесь присутствуют все возможные типы: некорректно распознанные буквы и цифры основной части ГРЗ, некорректно распознанные цифры кода региона, лишняя цифра в коде региона, а также неверно предсказанное отсутствие последней цифры.

Заключение

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

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Больше наших статей по машинному обучению и обработке изображений:

Подробнее..

Проецирование положения объектов с камеры видеонаблюдения на карту, используя лишь школьную геометрию

13.06.2021 16:20:24 | Автор: admin

Введение

Возможно ли превратить координаты на изображении в конкретные географические координаты? Несмотря на то, что это звучит несколько необычно, такая конвертация вполне возможна.

Сегодня я расскажу о том, как можно спроецировать координаты с плоского изображения на карту. Эта короткая статья будет своеобразным продолжением первой статьи, в которой я рассказывал о базовых возможностях Mask R-CNN.

Статья была написана в сотрудничестве с @avdosev за что ему большое спасибо.

Проблема

Необходимо по изображению с камеры понять, где находится объект на карте территории. Схематично это выглядит так.

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

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

Требования к камере:

Требование к изображению:

  • На изображении с камеры должна быть видна плоскость земли.

Способ 1.

Можно задать для каждого пикселя изображения соответствующую координату местности. При необходимости узнать геокоординату, мы просто ее получим по связанному пикселю.

Достоинства:

  • Алгоритм будет работать быстро, мы ограничены скоростью доступа к Map структуре данных, файлу или базе данных;

  • Сложных вычислений в рантайме нет.

Недостатки:

  • Если камера сдвигается даже на миллиметр, все наши данные устаревают, и будут давать уже не точный результат;

  • Требуется хранить информацию о каждом пикселе для каждой камеры на территории, что нецелесообразно;

  • Задавать для каждого пикселя координаты слишком долго и сложно.

Исходя из этих проблем, был отброшен первый способ и продолжен поиск других.

Способ 2.

Имея координаты углов области видимости камеры, можно вычислять координаты для подаваемого пикселя. Их можно получить двумя способами:

  1. Посмотреть на карту территории и изображения с камеры и постараться указать наиболее точные координаты углов;

  2. Зная параметры камеры (высота над землей, координаты камеры, угол наклона камеры и углы обзора), получить координаты углов видимости (трапеции), либо сразу вычислить координаты объекта.

Расчет положения объекта по координатам углов видимости

Для начала стоит разобраться, как выглядит область видимости камеры на виде сверху.

Для работы с этой областью, нужно отбросить часть плоского конуса рядом с камерой, которую я обозначил красным на рисунке выше. Эта область выбирается, исходя из того, что в красную зону не могут (или не должны) попасть объекты.

Область видимости камерыОбласть видимости камеры

Для простоты расчетов можно считать его трапецией.

Упрощенная до трапеции область видимости камерыУпрощенная до трапеции область видимости камеры

Для расчета местоположения объекта используем следующие формулы.

l_1 = \cfrac{C-B}{imageHeight - Y} + Bl_2 = \cfrac{D-A}{imageHeight - Y} + AM = \cfrac{l_2 - l_1}{imageWidth} * X + l_1

Где

  • l2 , l1 промежуточные переменные для вершины;

  • imageHeight, imageWidth высота и ширина изображения с камеры в пикселях соответственно;

  • A, B, C, D географические координаты вершин трапеции поля зрения камеры в формате {lat: float, lng: float};

  • X, Y координаты пикселей на изображении в декартовой системе координат, являются целыми числами;

  • M - результирующие координаты.

В случае Full HD картинки ширина и высота будут следующими: imageHeight=1080; imageWidth=1920.

После распознавания объекта на изображении, нейросеть отдаст координаты углов прямоугольника. Из него необходимо выбрать точку на изображении, для которой будут определяться координаты. Для этого есть несколько способов:

  1. Брать центроид прямоугольника;

  2. Брать середину нижней стороны прямоугольника. Этот способ даст более точный результат, если объект перемещается по земле, а не летает;

Всё это можно объединить:

Взять 1/N высоты и центр по горизонтали, где N может изменяться в зависимости от различных факторов, например, типа объекта или способа перемещения.

Например, для N=8 мы получим такую результирующую точку на прямоугольнике объекта.

Все эти способы имеют существенную погрешность при малой высоте камеры или/и при большом наклоне камеры.

Расчет углов видимости камеры, используя ее характеристики

Для нахождения точек A, B, C, D автоматизированным образом, нам необходимо найти центр будущей трапеции C.

Зная высоту h и угол наклона камеры , мы можем найти противоположный катет len.

len = h * \tan(\alpha)

Зная координаты камеры (точка О) и её направление (в какую сторону она смотрит, угол ) можно найти центр её наблюдения (точка С). Найти ее можно по формуле:

С_x = O_x + cos(\beta) * lenC_y = O_y + sin(\beta) * len

Измерить угол и на практике может быть затруднительным. Чтобы избежать сложных замеров, можно оценить примерную координату точки C (центр изображения), и вычислить углы.

\alpha = \arctan( \frac{\lvert С_x - О_x \rvert + \lvert C_y - O_y \rvert}{h})

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

Для основного угла +/- половина угла обзора по вертикали.

Для вторичного угла +/- половина угла обзора по горизонтали.

Примем горизонтальный угол обзора за viewAngleHorizontal, а вертикальный за viewAngleVertical.

Для точек, которые находятся ближе к камере, мы отнимем половину угла обзора, а для дальних - добавим.

Далее повторно рассмотрим точки трапеции. (Не стоит путать следующую точку C с центральной).

lenNear = h * tan(\alpha + viewAngleVertical / 2)A_x = O_x + cos(\beta - viewAngleHorizontal / 2 ) * lenNearA_y = O_y + sin(\beta - viewAngleHorizontal / 2 ) * lenNearB_x = O_x + cos(\beta - viewAngleHorizontal / 2 ) * lenNearB_y = O_y + sin(\beta - viewAngleHorizontal / 2 ) * lenNear lenFar = h * tan(\alpha - viewAngleVertical / 2)C_x = O_x + cos(\beta + viewAngleHorizontal / 2 ) * lenFarC_y = O_y + sin(\beta + viewAngleHorizontal / 2 ) * lenFarD_x = O_x + cos(\beta + viewAngleHorizontal / 2 ) * lenFarD_y = O_y + sin(\beta + viewAngleHorizontal / 2 ) * lenFar

Скомбинировав смещения по углам обзора, мы получаем координаты углов изображения - точки A, B, C, D.

Зная точки A, B, C, D можно получить географические координаты объекта. Но можно обойтись и без них. Следующий расчет потребует imageHeight, imageWidth, X, Y.

Если добавить вспомогательные оси, где координаты X, Y будут центром, то наш пиксель поделит изображение на 4 части. Определив отношения частей по горизонтали и по вертикали, мы можем определить углы, на которые должны делать смещение. Итоговая формула выглядит так:

len_M = h * tan(\alpha + viewAngleVertical * \frac{imageHeight - y}{imageHeight - 0.5})M_x = O_x + cos(\beta - viewAngleHorizontal * \frac{imageWidth - x}{imageWidth - 0.5} * len_MM_y = O_y + sin(\beta - viewAngleHorizontal * \frac{imageWidth - x}{imageWidth - 0.5} * len_M

Реализация на Python

imageWidth = 1920 # в данном примере зададим их константамиimageHeight = 1080import numpy as npdef geoToList(latlon):  return np.array((latlon['lat'], latlon['lng']))  def listToGeo(latlon):  return {'lat': latlon[0], 'lng': latlon[1] }  def getGeoCoordinates(A, B, C, D, X, Y):    A, B, C, D = list(map(geoToList, [A, B, C, D]))    vBC = (C - B) / imageHeight    vAD = (D - A) / imageHeight    latlonPixel1 = vBC * (imageHeight - Y) + B    latlonPixel2 = vAD * (imageHeight - Y) + A    vM = (latlonPixel2 - latlonPixel1) / imageWidth    M = vM * X + latlonPixel1    return listToGeo(M)

Результаты

Из этого изображения были получены координаты объекты левого верхнего и правого нижнего угла по X, Y соответственно - 613;233 1601;708.

Исходный код всегда доступен на Github.

Если найдете ошибки в алгоритме или в формулах, пожалуйста, сообщите об этом в комментариях.

Литература

Подробнее..

Распознавание дорожных знаков

24.04.2021 18:16:03 | Автор: admin

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

Набор данных дорожных знаков

В рамках этой статьи используется общедоступный набор данных, доступный вKaggle:GTSRB это мультиклассовая задача классификации одного изображения, которая проводилась на Международной совместной конференции по нейронным сетям (IJCNN) 2011. Набор данных содержит более 50 000 изображений различных дорожных знаков и классифицируется на 43 различных класса. Он весьма разнообразен: некоторые классы содержат много изображений, а некоторые классы - несколько изображений.

Изучение набора данных

В начале импортируем все необходимые библиотеки.

import osimport matplotlibimport numpy as npfrom PIL import Imagefrom tensorflow.keras.preprocessing.image import img_to_arrayfrom sklearn.model_selection import train_test_splitfrom keras.utils import to_categoricalfrom keras.models import Sequential, load_modelfrom keras.layers import Conv2D, MaxPool2D, Dense, Flatten, Dropoutfrom tensorflow.keras import backend as Kimport matplotlib.pyplot as pltfrom sklearn.metrics import accuracy_score

Для тренировки нейронной сети будем использовать изображения из папки train, которая содержит 43 папки отдельных классов. Инициализируем два списка:dataи labels. Эти списки будут нести ответственность за хранение наших изображений, которые мы загружаем, вместе с соответствующими метками классов.

data = []labels = []

Далее, с помощью модуля os мы перебираем все классы и добавляем изображения и их соответствующие метки в списокdataиlabels. Для открытия содержимого изображения используется библиотекаPIL.

for num in range(0, classes):    path = os.path.join('train',str(num))    imagePaths = os.listdir(path)    for img in imagePaths:      image = Image.open(path + '/'+ img)      image = image.resize((30,30))      image = img_to_array(image)      data.append(image)      labels.append(num)

Этот цикл просто загружает и изменяет размер каждого изображения до фиксированных 3030 пикселей и сохраняет все изображения и их метки в спискахdataиlabels.

Затем нужно преобразовать список в массивыnumpyдля подачи в модель.

data = np.array(data)labels = np.array(labels)

Форма данных - (39209, 30, 30, 3), означает, что имеется 39 209 изображений размером 3030 пикселей, а последние 3 означают, что данные содержат цветные изображения (значение RGB).

print(data.shape, labels.shape)(39209, 30, 30, 3) (39209,)

Из пакета sklearn мы используем метод train_test_split() для разделения данных обучения и тестирования, используя 80% изображений для обучения и 20% для тестирования. Это типичное разделение для такого объема данных.

X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.2, random_state=42)print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)(31367, 30, 30, 3) (7842, 30, 30, 3) (31367,) (7842,) 

Давайте проверим, сколько классов у нас есть и сколько изображений в обучающем наборе для каждого класса и построим диаграмму распределения классов.

def cnt_img_in_classes(labels):    count = {}    for i in labels:        if i in count:            count[i] += 1        else:            count[i] = 1    return countsamples_distribution = cnt_img_in_classes (y_train)def diagram(count_classes):    plt.bar(range(len(dct)), sorted(list(count_classes.values())), align='center')    plt.xticks(range(len(dct)), sorted(list(count_classes.keys())), rotation=90, fontsize=7)    plt.show()diagram(samples_distribution)
Диаграмма распределенияДиаграмма распределения

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

def aug_images(images, p):    from imgaug import augmenters as iaa    augs =  iaa.SomeOf((2, 4),          [              iaa.Crop(px=(0, 4)),               iaa.Affine(scale={"x": (0.8, 1.2), "y": (0.8, 1.2)}),              iaa.Affine(translate_percent={"x": (-0.2, 0.2), "y": (-0.2, 0.2)}),              iaa.Affine(rotate=(-45, 45))              iaa.Affine(shear=(-10, 10))])        seq = iaa.Sequential([iaa.Sometimes(p, augs)])    res = seq.augment_images(images)    return resdef augmentation(images, labels):    min_imgs = 500    classes = cnt_img_in_classes(labels)    for i in range(len(classes)):        if (classes[i] < min_imgs):            add_num = min_imgs - classes[i]            imgs_for_augm = []            lbls_for_augm = []            for j in range(add_num):                im_index = random.choice(np.where(labels == i)[0])                imgs_for_augm.append(images[im_index])                lbls_for_augm.append(labels[im_index])            augmented_class = augment_imgs(imgs_for_augm, 1)            augmented_class_np = np.array(augmented_class)            augmented_lbls_np = np.array(lbls_for_augm)            imgs = np.concatenate((images, augmented_class_np), axis=0)            lbls = np.concatenate((labels, augmented_lbls_np), axis=0)    return (images, labels)X_train, y_train = augmentation(X_train, y_train)

После увеличения наш обучающий набор данных имеет следующую форму.

print(X_train.shape, X_test.shape, y_train.shape, y_test.shape)(36256, 30, 30, 3) (7842, 30, 30, 3) (36256,) (7842,)

Давайте еще раз проверим распределение данных.

augmented_samples_distribution = cnt_img_in_classes(y_train)diagram(augmented_samples_distribution)
Диаграмма распределения после аугментацииДиаграмма распределения после аугментации

На графика видно, что наш набор стал более сбалансирован. Далее из пакета keras.utils мы используем метод to_categorical для преобразования меток, присутствующих вy_trainиt_test, в one-hot encoding.

y_train = to_categorical(y_train, 43)y_test = to_categorical(y_test, 43)

Построение нейронной сети

Для создания нейронной сети будет использоваться библиотекаKeras]. Чтобы классифицировать изображения по соответствующим категориям, мы построим модель CNN (сверточная нейронная сеть). CNN лучше всего подходит для целей классификации изображений.

Архитектура нашей модели:

  • 2 Conv2D слоя (filter=32, kernel_size=(5,5), activation=relu)

  • MaxPool2D слой ( pool_size=(2,2))

  • Dropout слой (rate=0.25)

  • 2 Conv2D слоя (filter=64, kernel_size=(3,3), activation=relu)

  • MaxPool2D слой ( pool_size=(2,2))

  • Dropout слой (rate=0.25)

  • Flatten слой, чтобы сжать слои в 1 измерение

  • Dense слой (500, activation=relu)

  • Dropout слой (rate=0.5)

  • Dense слой (43, activation=softmax)

class Net:  @staticmethod  def build(width, height, depth, classes):    model = Sequential()    inputShape = (height, width, depth)    if K.image_data_format() == 'channels_first':      inputShape = (depth, heigth, width)    model = Sequential()    model.add(Conv2D(filters=32, kernel_size=(5,5), activation='relu', input_shape=inputShape))    model.add(Conv2D(filters=32, kernel_size=(5,5), activation='relu'))    model.add(MaxPool2D(pool_size=(2, 2)))    model.add(Dropout(rate=0.25))    model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu'))    model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu'))    model.add(MaxPool2D(pool_size=(2, 2)))    model.add(Dropout(rate=0.25))    model.add(Flatten())    model.add(Dense(500, activation='relu'))    model.add(Dropout(rate=0.5))    model.add(Dense(classes, activation='softmax'))    return model

Обучение и проверка модели

Мы строим нашу модель вместе с оптимизатором Adam, а функция потерь это categorical_crossentropy, потому что у нас есть несколько классов для категоризации. Затем обучаем модель с помощью функции model.fit().

epochs = 25model = Net.build(width=30, height=30, depth=3, classes=43)model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])history = model.fit(X_train, y_train, batch_size=64, validation_data=(X_test, y_test), epochs=epochs)

Как вы можете видеть, наша модель обучалась в течении 25 эпох и достигла 93% точности на тренировочном наборе данных. С помощью matplotlib мы строим график для точности и потерь.

plt.style.use("plot")plt.figure()N = epochsplt.plot(np.arange(0, N), history.history["loss"], label="train_loss")plt.plot(np.arange(0, N), history.history["val_loss"], label="val_loss")plt.plot(np.arange(0, N), history.history["accuracy"], label="train_acc")plt.plot(np.arange(0, N), history.history["val_accuracy"], label="val_acc")plt.title("Training Loss and Accuracy")plt.xlabel("Epoch")plt.ylabel("Loss/Accuracy")plt.legend(loc="lower left")plt.show()
Training Loss and AccuracyTraining Loss and Accuracy

Тестирование модели на тестовом наборе

Набор данных содержит папку Test, а в файле Test.csv есть сведения, связанные с путем к изображению и метками классов. Мы извлекаем путь к изображению и метки из файла Test.csv с помощью фреймворка Pandas. Затем, мы изменяем размер изображения до 3030 пикселей и делаем массив numpy, содержащий все данные изображения. С помощью accuracy_score из sklearn metrics проверяем точность предсказаний нашей модели. Мы достигли 96% точности на этой модели.

y_test = pd.read_csv('Test.csv')labels = y_test["ClassId"].valuesimgs = y_test["Path"].valuesimages=[]for img in imgs:    image = Image.open(img)    image = image.resize((30,30))    images.append(img_to_array(image))X_test=np.array(images)pred = model.predict_classes(X_test)print(accuracy_score(labels, pred))0.958590657165479
Подробнее..

Распознавание волейбольного мяча на видео с дрона

14.06.2021 16:14:45 | Автор: admin

В прошлом году я развлекался треккингом волейбольного мяча, используя удаление фона OpenCV с анализом траекторий и даже сделал сервис, который на основе этой информации вырезает скучные моменты из игры.

Основным фидбеком было - что за каменный век использовать олдскульные технологии, обучаешь нейросеть и погнали. Я пробовал, но не взлетело - очень часто мяч настолько размыт, что его даже человеческим глазом не отличить от случайной кляксы, и даже простой бинарный классификатор не дает стабильных результатов, чего уже говорить о детекторах и прочих YOLO.

В общем, я это дело подзабросил, но вот весной завел себе дрон и конечно же первым делом приспособил его для волейбольных съемок. Если поднять его на нужную высоту (12-15 метров), в кадр влезает вся площадка, что дает прямо неограниченные возможности для анализа.

Камера дрона держится на гимбале, который гасит колебания аппарата и на выходе получается стабильная картинка. На эту картинку я и направил свой алгоритм распознавания, но ...все оказалось не так то просто.

Шум и никакого мячаШум и никакого мяча

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

Для сравнения - похожая подача при старом подходе выглядит вот так:

Делать нечего, придется искать другие подходы. У нас есть OpenCV, так что далеко идти все равно не придется. Вместо фона выделяем границы помощью фильтра Канни - и у нас есть черно-белый скетч для дальнейшего разбора.

      gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)      gray = cv.GaussianBlur(gray, (5, 5),0)      mask = cv.Canny(gray, 50, 100)

Если натравить на него детектор контуров - все прекрасно работает, мяч удается зацепить. Дальше подключается детектор траекторий и процесс идет как и в основном алгоритме.

      mask = backSub.apply(frame)      mask = cv.dilate(mask, None)      mask = cv.GaussianBlur(mask, (15, 15),0)      ret,mask = cv.threshold(mask,0,255,cv.THRESH_BINARY | cv.THRESH_OTSU)

И опять же для сравнения - тот же фрагмент с неподвижной камеры с границами:

Как видно, палка имеет два конца - на трясущемся дроне Канни-фильтр дает лучшие результаты, а на статичной камере - генерит лишний фон, на котором теряется мяч.

Но что будет, если эти два подхода совместить? Для видео с дрона Канни сначала оставит только нужные фигуры, а затем, можно удалить фон.

Получается картинка не без шума, но его можно уже фильтровать, и мяч уже видно.

Результат - гораздо лучше, шума меньше (но есть еще) и траектория мяча распознается достаточно четко.

Прошлые статьи на эту же тему

Подробнее..

Рисуем светом длинная выдержка на Android

21.05.2021 22:21:42 | Автор: admin

Всем привет, меня зовут Дмитрий, и я Android-разработчик в компании MEL Science. Сегодня я хочу рассказать, как можно реализовать поддержку длинной выдержки на смартфонах, да так, чтобы получающуюся картинку можно было наблюдать прямо в процессе создания. А для заинтересовавшихся в конце статьи я подготовил ссылку на тестовое приложение - чтобы вы могли сами сделать крутое фото с длинной выдержкой.

Длинная выдержка

Выдержка - термин из мира фотографии, который определяет время открытия затвора при съемке. Чем дольше открыт затвор, тем дольше свет экспонирует светочувствительную матрицу. Проще говоря, делает фотографию более яркой. Современные фотоаппараты используют выдержки длинной в 1/2000 cекунды, что позволяет получить освещенную, но при этом не пересвеченную фотографию. Длинная выдержка подразумевает открытие затвора на секунду и больше. При верно выбранной сцене это позволяет получать фантастические фотографии, способные запечатлеть движение света в объективе камеры. Причем фотографировать можно что угодно: ночные улицы с мчащимися машинами или маятник, с укрепленным на нем фонариком, позволяющим выписывать фигуры Лиссажу. А можно вообще рисовать светом самому и получать целые картины-фотографии.

Улицы города, сфотографированные с использованием длинной выдержкиУлицы города, сфотографированные с использованием длинной выдержки

Улицы города, сфотографированные с использованием длинной выдержки

Теория

Для создания эффекта длинной выдержки можно использовать два подхода:

  • аппаратный - состоит в управлении физическим открытием и закрытием затвора

  • программный - эмулирует длинную выдержку за счет объединения большого числа обычных кадров

Главным недостатком аппаратного подхода является отсутствие возможности наблюдать за процессом появления фотографии онлайн - результат будет виден лишь после закрытия затвора и формирования изображения. Нарисовать что-либо светом человеку без опыта в таком режиме вряд ли удастся. Еще одним недостатком становится ограничение смартфонов на максимальное время выдержки - на Android оно составляет 30 секунд.

Программная эмуляция позволяет избавиться от всех недостатков ценой усложнения кода.

Практика

Для реализации работы с камерой смартфона будем использовать API CameraX. Это обусловлено ее гибкостью и лаконичностью. Также для программного подхода нам потребуется OpenGL ES для работы с изображениями. Данный выбор был сделан так как, это позволит работать напрямую с изображениями в видео памяти и обеспечить минимальную задержку при записи, так как вся обработка изображений происходит в реальном времени.

Аппаратный подход

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

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

val imageCaptureBuilder = ImageCapture.Builder()Camera2Interop.Extender(imageCaptureBuilder).apply {   setCaptureRequestOption(    CaptureRequest.CONTROL_AE_MODE,    CaptureRequest.CONTROL_AE_MODE_OFF  )  setCaptureRequestOption(    CaptureRequest.SENSOR_EXPOSURE_TIME,    EXPOSURE_TIME_SEC * NANO_IN_SEC  )}

Кстати, для каждой камеры диапазон допустимого интервала выдержки может быть разным, чтобы его узнать необходимо запросить cameraCharacteristics

val manager = getSystemService(CAMERA_SERVICE) as CameraManagerfor (cameraId in manager.cameraIdList) {  val chars = manager.getCameraCharacteristics(cameraId)  val range = chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)    Log.e("CameraCharacteristics", "Camera $cameraId range: ${range.toString()}")}

Программный подход

Для начала определимся с общей идеей нашей реализации.

  1. Нам потребуется буффер для хранения формирующегося изображения, а также регулярно обновляемое изображение с камеры.

  2. Каждый новый кадр будем объединять с хранящимся в буффере, обрабатывая попиксельно и оставляя в буфере пиксель с наибольшей яркостью.

  3. Для того чтобы придать нашим картинкам эффект постепенного исчезновения света можем долго неизменяемые пиксели постепенно затемнять.

Я не буду приводить здесь весь код рендеринга, так как это займет слишком много места. Для желающих код можно найти здесь. Представлю лишь блок схему, объясняющую последовательность действий для получения очередного кадра на экране.

Как видно из схемы, основная магия происходит при объединении 2х кадров: сохраненного в фреймбуфере и полученного с камеры. Рассмотрим шейдер для этой задачи подробнее.

#extension GL_OES_EGL_image_external : requireprecision mediump float;uniform mat4 stMatrix;uniform texType0 tex_sampler;uniform texType1 old_tex_sampler;varying vec2 v_texcoord;void main() {        vec4 color = texture2D(tex_sampler, (stMatrix * vec4(v_texcoord.xy, 0, 1)).xy);    vec4 oldColor = texture2D(old_tex_sampler, v_texcoord);      float oldBrightness = oldColor.r * 0.2126 + oldColor.g * 0.7152 + oldColor.b * 0.0722 + oldColor.a;     float newBrightness = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722 + color.a;  // объединяем пиксели}

Работа шейдера состоит из нескольких этапов:

  1. К текстуре камеры мы применяем матрицу для получения верной ориентации изображения.

  2. Затем вычисляем яркость пикселя на обоих кадрах

  3. Объединяем пиксели

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

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

Тогда объединение пикселей будет выглядеть вот так:

if (newBrightness > oldBrightness) {  gl_FragColor = color;} else {  gl_FragColor = oldColor;}

Попробуем запустить такой код и обнаружим, что эффект длинной выдержки действительно достигается. Вот что получилось.

Длинная выдержкаДлинная выдержка

Длинная выдержка

Однако любая ошибка при рисовании требует перезапуска камеры, т.к. один раз попавший на нее свет уже нельзя стереть! Такое поведение приемлемо при выполнении каких-то заранее спланированных фотографий, но что если мы хотим просто рисовать светом и сохранять изображение, лишь когда нам действительно понравился результат? Перезапускать постоянно камеру совсем неудобно. Значит свет все-таки должен пропадать через какое то время. Этого можно добиться с помощью постепенного затухания ярких пикселей. Чтобы добиться такого эффекта достаточно просто на каждом новом шаге добавлять к каждому пикселю немного черного цвета (чтобы сохранять корректность картинки мы будем добавлять не черный цвет, а просто более темный пиксель из доступных - это позволит и эффект угасания получить и сохранить гамму цветов). Тогда объединение пикселей будет выглядеть следующим образом

if (newBrightness > oldBrightness) {    gl_FragColor = mix(color, oldColor, 0.01);} else {   gl_FragColor = mix(oldColor, color, 0.01);}

Вот несколько примеров с разными коэффициентами и временем затухания света.

Коэффициент 0.001Коэффициент 0.001

Коэффициент 0.001

Коэффициент 0.01Коэффициент 0.01

Коэффициент 0.01

Коэффициент 0.5Коэффициент 0.5

Коэффициент 0.5

Заключение

 Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы? Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы?

Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы?

На этом на сегодня все. Для желающих попробовать самому полный код приложения и apk можно найти здесь.

Подробнее..

FOVEA томографируем коня через игольное ушко

27.05.2021 14:07:09 | Автор: admin

Рентгеновская томография - один из двух (наряду с МРТ) самых известных способов заглянуть внутрь непрозрачных объектов. В медицине он является инструментом клинического мониторинга и средством терапии, в индустрии помогает контролировать технологические процессы, в таможне - найти то, что кое-кто предпочел бы спрятать. Эта технология в нашей стране развивается in house на мировом уровне. Но мы в Smart Engines пишем про томографию так часто не только поэтому. Мы - ученые и изобретатели, а томография - неиссякаемый источник проблем и задач, требующих решения (мы уже писали о несовершенных детекторах и широкополосном излучении). Сегодня мы расскажем о том, что делать, если объект исследования не помещается в томограф. Вот как, например, британские ученые исследуют коня в зоопарке. Голову коня в гентри поместить удается, а с остальным дела обстоят сложнее. Пример не очень серьезный, но жизненный. Кто в лаборатории работал, тот в зоопарке не смеется. Заглянув под кат вы узнаете, как получается, что у физиков сантиметровый образец не помещается в километровый томограф, и чем тут могут помочь вычислительные математики.

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

Впрочем, для боди-позитивного маскота можно было бы построить гентри побольше - в чем проблема? Но не все томографические комплексы вообще используют гентри. Есть такая машина - синхротрон. Машиной их называют специалисты, и это может вызвать неправильные образы при первом знакомстве. Размеры такой машины достигают километров, а строятся они, бывает, всем миром. В полном смысле этого слова. Например, синхротрон ESRF, расположенный во французском городе Гренобле, был построен в 1994 году совместными усилиями 20 стран. Синхротрон дает мощнейший узконаправленный пучок рентгеновских лучей. Такой мощный, что время многих измерений сокращается в десятки и сотни тысяч раз. Для регистрации рентгеновского излучения на его станциях используются уникальные плоскопараллельные детекторы с высочайшим пространственным разрешением. Излучатель и детектор остаются неподвижными, а образец вращается. Часто размеры исследуемых объектов, мельчайшие детали которых требуется рассмотреть, большие, а вот размеры детекторов ограничены.

Давайте посмотрим на результат измерений типичного биологического объекта в такой томографической схеме. Это фрагмент кости, отснятый с субмикронным разрешением. Измерения проведены на современном швейцарском синхротроне Swiss Light Source Paul Scherrer Institut. Намётанный глаз позволяет увидеть и контуры объекта, и трабекулы (перегородки) внутри. Но при таком разрешении объект не помещается целиком в поле зрения детектора. На каждой из проекций просматривается только часть объекта.

Похожее мы наблюдали в детстве, когда подсматривали в замочную скважину. Мы видели только часть запертой от нас комнаты. Заглядывая в нее под разными углами, можно расширить поле зрения, увидеть больше, но общую картину придется сшивать силой воображения. Традиционные алгоритмы томографической реконструкции для такого не предназначены.

Чтобы лучше почувствовать проблему, обратимся, как это принято среди ученых, к модели. Возьмем в качестве модельного объекта грецкий орех, совсем недавно оттомографированный в разных режимах нашими коллегами из Голландии. На картинке ниже показан орех и его рентгеновское изображение.

По структуре орех похож на кость, и для него нам известен ground truth - реконструкция, полученная при полных данных.

Теперь посмотрим, что мы сможем реконструировать при измерениях через замочную скважину. На рисунке ниже красным прямоугольником показано поле зрения нашего воображаемого детектора.

При таком соотношении размеров вместо целой синограммы объекта (что это такое, мы уже писали на Хабре) будет видна только ее часть (ниже в красной рамке).

Поскольку в детектор помещается больше половины ореха, то при вращении все его части когда-нибудь попадают в поле зрения детектора. Сама же синограмма имеет характерную структуру и кажется, что её фрагмент возможно экстраполировать до полного размера.

Будем строить такую синограмму, чтобы 1) она в области измерений совпадала с экспериментальной синограммой, 2) существовал объект, которому соответствовала бы наша синограмма (это нетривиальный факт, но не любая функция является синограммой). Предложенный нами для этого итерационный алгоритм называется Field Of View Extension Algorithm - FOVEA (любопытные хабровчане могут порыться в интернете и отгадать, почему аббревиатура составляет именно это слово).

Вот пошаговое описание алгоритма:

  1. Создадим массив D_{iterative} , заполненный 0 . В нём мы будем итеративно обновлять значения элементов восстанавливаемого цифрового объёма.

  2. Рассчитаем синограммы S_{iterative} от D_{iterative} .

  3. Заменим рассчитанные на Шаге 2 значения S_{iterative} доступными нам экспериментальными значениями - S_{experimental} .

  4. Проведём томографическое восстановление D_{iterative} из синограммы S_{iterative} методом FBP.

  5. Рассчитаем синограмму S_{iterative} от D_{iterative} .

  6. Сравним (например, рассчитав L2 норму) S_{iterative} и S_{experimental} в той области, где мы знаем экспериментальные значения. Если расхождение малое, т.е. экспериментальная и модельная синограммы совпадают, то конец расчёта: искомая реконструкция лежит в D_{iterative} . В противном случае переходим на Шаг 3.

Ниже приведены результаты реконструкции обрезанной синограммы методами FBP и FOVEA.

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

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

Над методами томографии высокого пространственного разрешения продолжают биться ученые всего мира. Ведь так хочется увидеть каждый нейрон в нашей голове и разгадать, наконец, как работает человеческий нейрокомпьютер. Для этого нужно нанометровое разрешение. 1 кубический миллиметр содержит квинтиллион вокселей размером 1 нанометр. Если значение вокселя кодируется числом с плавающей точкой одинарной точности (float32), то только для хранения результатов реконструкции потребуется 4 эксабайта (4 106 Тбайт) памяти. Но в голове ни много ни мало, а порядка 30 миллионов таких кубических миллиметров. Поэтому сегодня в высоком разрешении томографируют не весь мозг, а отдельные его участки, для чего их необходимо извлечь из головы. Что-то нам подсказывает, что методы томографирования через замочную скважину будут актуальными еще долго...

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru