Введение

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

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

На диплом художник выбрал Российскую национальную библиотеку (они выбирают объекты, и декорируют их). В итоге - 9 отдельных картин предполагаемых в технике мозаика были готовы украшать фасад здания. Сделать диплом - это полдела, но более важной задачей является подать его. По задумке камера должна вальяжно облетать здание, масштабироваться и проходить по замысловатым траекториям. Но вот незадача, курс 3D моделирования длился полгода, а результатом была модель пустой комнаты, с плинтусами и окнами. Отчаянные просмотры роликов на YouTube по темам «Как сделать 3D иллюстрации Adobe» дали понять одно - 3D визуализации не будет. Больно, грустно, обидно - но дедлайн заставляет креативить.

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

Мое глубокое подсознание положило эту мысль в стек головного мозга и достало его оттуда, как это водится, перед самым сном. «Так ведь можно это все реализовать программно. Распознать рамки под панно какой-нибудь нейронкой, вставить мозаики попутно сжимая их и растягивая в нужных местах, затенить, выделить контраст и бла бла бла» подумал наш герой-программист и его было уже не остановить…

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

Учитывая всё вышесказанное, формировались следующие задачи:

  1. Научиться определять рамки под мозаики

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

  3. Фильтровать все неугодные пиксели: столбы, провода, листочки и пролетающих птичек

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

Разметка первого фрейма

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

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

Российская национальная библиотека. Левее - исходное изображение с выделенными контурами, правее - изображение с разметкой.
Российская национальная библиотека. Левее - исходное изображение с выделенными контурами, правее - изображение с разметкой.

Идея не нова, и заключается в том, что необходимо выделить направляющие рамок максимально неестественными цветами, которые легко можно определить программно. Так хватило 3-х цветов:

  • Зеленым (0, 255, 0) определяются границы кривых контуров (полукруглые линии) и вертикальные направляющие.

  • Синим (0, 0, 255) - горизонтальные границы крайних рамок. Можно было так же зеленым, но я подумал что так легче разграничить прямые и кривые линии .

  • Красным (255, 0, 0) - все объекты, которые находятся перед библиотекой и её перекрывают (совсем мелкие объекты, например некоторые провода, игнорировались, так как не так сильно заметны).

Почему не точная разметка контуров? Так показалось проще и быстрее. Определить визуально точные рамки контуров сложно и это отнимает много времени, а вот разместить направляющие, найти их пересечения - вроде как проще. На первое изображение у меня ушло минут 30, на остальные - около 5-10 минут.

Приведение изображений к нужным форме и пропорциям

Идеальные фигуры встречаются только в геометрии.

Чтение линий с изображений

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

Для тех же прямых линий выглядит это так
lines = cv2.HoughLinesP(mask_blue, rho=5.0, theta=np.pi / 70, threshold=100)

Параметры rho, theta и threshold подбирались экспериментально. Как мне показалось, эта не та задача где можно применять это преобразование. Для других линий даже не стал пытаться искать коэффициенты, тем более для других фотографий библиотеки.

А вот функция cv2.findContours(...) оказалась той что нужно. Естественно, нужно сделать из контуров линии, но пара строчек кода с группировкой по строкам или столбцам, в зависимости от линии, решили проблемы. 

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

Выделенные контуры для вставки картин по заданной разметке
Выделенные контуры для вставки картин по заданной разметке

Перспектива и деформация

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

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

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

Для перспективы, в opencv имеется функция cv2.getPerspectiveTransform(,,,), а если точнее, эта функция строит матрицу трансформации, произведение с которой и обеспечивает перспективу. Для функции необходимо составить матрицы входных и выходных рамок, записывая углы самих рамок. Далее с помощью функции “cv2.warpPerspective(...)” происходит преобразование. Более подробно всё описано здесь.

Матрица для деформации, если кому вдруг интересно

0.925

-0.12

30.

-0.075

0.88

30.

0.

0.

1.

Нет проекции (0, 0), сдвиг по двум осям равен (30, 30), а вот поворот, перемасшабирование и т.д. задается матрицей с вещественными числами.

Демонстрация перспективы картины
Демонстрация перспективы картины

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

С деформацией алгоритм очень похож:

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

  2. Рассчитать матрицу преобразования с помощью skimage.transform.PiecewiseAffineTransform(...)

  3. Преобразовать изображение используя skimage.transform.warp(...).

Демонстрация деформации картины
Демонстрация деформации картины

Таким образом, каждое преобразование ресайзится и преобразуется в зависимости от заданной рамки.

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

То, что не замечено ранее несет с собой ведро с помоями

Первый «Привет» прилетел от артефактов при деформации. Сходу прикреплю пример чтобы было наглядней.

Тут смотреть нужно на черную обводку вокруг изображения. Вставить картину на её законное место дело то нехитрое:

  1. Подготовить маску, по которой изображение будет вставлено - там где в маске единицы, пиксели будут скопированы:

    1. mask = np.ones(picture_shape)

    2. mask[picture == (0, 0, 0)] = 0 - черные пиксели границ изображения заполнить нулями.

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

  3. Перенести наше деформированное творчество.

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

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

  1. Создать матрицу коэффициентов [0, 1], соответствующую маске (где единицы  изображение, нули - пустота).

  2. Идти окном (3, 3) с помощью замечательной функции skimage.util.view_as_windows(...), и без жалости усреднять значения коэффициентов. Несколько раз. Ближе к границам коэффициенты распределятся по градиенту от нуля до единицы.

  3. Умножаем изображение мозаики на 1 - коэффициенты. Там где коэффициенты равны единице изображение не изменится, а в усредненных местах понизится контрастность.

  4. Умножаем фон на коэффициенты, с той же логикой

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

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

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

Мои рассуждения

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

Могло бы быть и лучше

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

Результат интеграции картин в формате gif с заметной тряской
Результат интеграции картин в формате gif с заметной тряской

Очевидное решение - найти эти смещения каждого фрейма относительно первого, а после сдвинуть текущий фрейм чтобы сравнять его с первым. Google подсказал метод “cv2.phaseCorrelate(...)“, оперирующий фазовой корреляцией. Получилось быстро и легко, но вот только качество получилось так себе. Метод находит смещение, но не так точно как этого бы хотелось. Результат дергается чуть меньше, но все равно дергается.

Чтобы быть до конца уверенным, попробовал также итеративный подход: необходимо задать максимально возможное смещение (в моей случае это было \pm 10по вертикали), и смещая второе изображение, измерять коэффициент взаимной корреляции. Не очень эффективный подход, но ради эксперимента приемлемо. Исходное видео и 2 подхода совместил и поместил ниже.

Склейка оригинального видео и двух подходов стабилизации. Слева - исходное видео, в центре phaseCorrelate и справа - итеративный подход.
Склейка оригинального видео и двух подходов стабилизации. Слева - исходное видео, в центре phaseCorrelate и справа - итеративный подход.

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

P. S. Буду рад замечаниям и предложениям (ссылка на проект - GitHub).

Комментарии (10)


  1. mazeoff
    12.07.2022 00:09

    Почему python, а не C++/OpenCV?


    1. IvanHod Автор
      12.07.2022 00:40
      +3

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


    1. t38c3j
      12.07.2022 01:05
      +1

      Разве cv2 это не биндинг на нативную реализацию с numpy?


      1. IvanHod Автор
        12.07.2022 12:08

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


  1. mayorovp
    12.07.2022 09:32
    +1

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

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


    1. IvanHod Автор
      12.07.2022 11:58

      А я всё думал, как полупрозрачный использовать. Полез в cv2.BORDER_TRANSPARENT, но так и не разобрался.
      Попробуй заполнять прозрачным, спасибо!


  1. redfraction
    12.07.2022 10:53
    -2

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


    1. IvanHod Автор
      12.07.2022 12:04

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

      К тому же у девушки на это не было времени. А я между выбором "копаться в 3d визуализации" и "обрабатывать изображения с помощью python" - определенно бы выбрал второй вариант) Плюс точно смог бы сказать, что пусть минимальная визуализация таким образом получится за те же 60 часов.


  1. max_valo
    12.07.2022 23:34

    Круто, красавчик! Мне кажется для большей достоверности осталось как то выстроить работу с тенями:)


    1. IvanHod Автор
      12.07.2022 23:38

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