Оглавление. На прошлом уроке я рассказал о том, как повысить контрастность изображения и как выделить на изображении особые точки. Затем мы попробовали работать с найденными особыми точками. В частности, превратили эти точки в список  координат и попытались объединить близкие точки в одну, так как у нас получилось очень много точек рядом. В статье был предложен следующий алгоритм: при составлении списка, перед добавлением в список очередной точки проверять, находится ли она близко от последней, если да, то добавлять в тот же список, если нет, то начинать новый список.  Только проблема в том, что обход точек был через развертку, и могло получиться так, что близкие точки попадают в разные списки. Поэтому объединение точек получилось «криво».  Сегодня мы исправим этот недочет. Для начала, почему мы вообще начали все эти хитрости с алгоритмом? Дело в том, что если решать задачу «в лоб», то время работы алгоритма у нас будет пропорционально квадрату размера списка. Собственно, давайте сначала проверим, насколько критично будет время работы алгоритма «в лоб» при разных ситуациях. И так, нам нужно перебрать все точки и сравнить их расстояния со всеми точками: Вот что у нас вышло: На моем компе (Intel(R) Core(TM) i3-8350K CPU @ 4.00GHz   4.01 GHz, памяти 16 Гб) время работы составило 0,5 секунды. Точек, напоминаю, 2829, после обработки осталось 146.  Для разового запуска это, конечно, мелочь, а вот для программы, работающей в режиме реального времени, это критично. Хуже, если кол-во точек повысить. В своем эксперименте я повысил их количество до 12247, просто поменяв порог чувствительности. Теперь программа выполнялась уже 10 секунд, оставив после объединения 455 точек.  Как же решить эту проблему? Если честно, то обсуждение теории алгоритмов выходит за рамки цикла уроков о компьютерном зрении, но все-таки, я поделюсь с вами одной идейкой, как ускорить процесс объединения точек. Не нужно перебирать список для каждого из его элементов. Можно перебрать его один раз, сразу помещая элемент (точку) в определенный список (кластер). Правда, тут, возникает проблема. Мы не знаем заранее, какие будут кластеры. Не беда. Давайте создавать их по мере перебора. Сначала у нас нет ни одного кластера. Поэтому мы создаем один и помещаем в него нашу точку. Точнее, помещать ее не нужно. Просто запомним ее координаты, а для следующих точек будем корректировать эти координаты кластера, которые, по сути, центр масс входящий в кластер точек. Если окажется, что точка слишком далека от центра кластера, создаем точно так же новый кластер. Вот, собственно, и сам алгоритм: Теперь программа выполнялась 0.08 сек, и осталось после объединения 127 точек. «Что-то не сходиться», - скажете вы. Да, совершенно верно, результаты будут отличаться от тех, что были в предыдущем алгоритме. Да, мы ускорили обработку, но немного пожертвовали точностью.  Теперь поговорим о том, как нам этот набор точек превратить в граф. Чисто для того, чтобы научится, построим самый примитивный граф: начнем построение с какой-нибудь точки, соединим ее с ближайшей, ту со следующей ближайшей и так до тех пор, пока не обойдем все точки.  Это называется жадный алгоритм, и мы, по сути, решили задачу коммивояжера,  которая состоит в том, чтобы обойти все пункты по кратчайшему пути (ну, или почти по кратчайшему).  Поясню почему «почти по кратчайшему». Дело в том, что задача коммивояжера относится к классу NP-трудных задач. Наикратчайший путь возможно найти только полным перебором, но беда в том, что с увеличением количества пунктов время полного перебора возрастает очень резко. Алгоритмическая сложность такой задачи O(n!). То есть, если количество пунктов, которые надо обойти, равно 20, то нам потребуется 2 432 902 008 176 640 000 итераций. Пусть даже в секунду компьютер делает миллиард итераций, тогда для отработки алгоритма потребуется примерно 77 лет. А если итераций будет 21, то времени потребуется еще в 21 больше.  В общем, ясно, что полный перебор в данном случае – это не метод решения. Поэтому задачу поиска наикратчайшего пути заменяют задачей нахождение более-менее короткого пути («почти наикратчайшего» как было замечено выше).  В качестве «какой-нибудь» точки мы возьмем центр наиболее «весомого» кластера (того, за который «проголосовало» наибольшее количество рядом стоящих точек). Вот как мы его найдем, одновременно рисуя точки на экране: Потом, конечно, удалим эту точку из списка, и создадим список ребер графа: Осуществим обход точек жадным алгоритмом, так как точек немного, можно решить задачу «в лоб»,  то есть через полный перебор точек в двойном цикле: Ну, и наконец, переберем наши ребра, чтобы отобразить их на экране: Вот что у нас получится: Возможно, кто-то спросит: «А какова практическая польза в этих вот красивых построения?». Отвечу. Вот представьте, что вам надо распознать на изображении какой-то объект (например, котика, человеческое лицо, дом, автомобиль). Сложность задачи заключается в том, что объект может находится в разных местах картинки, он может быть повернут под различным углом, может быть большим или маленьким (далеким и близким). Кроме того, объект может быть снят с разных ракурсов. Таким образом, нам необходимо каким-то образом сделать так, чтобы объект был инвариантен к переносу, к вращению, к изменению размера, и, желательно, к аффинным преобразованиям. Граф – это как раз пример такой структуры. Вот смотрите. Если у нас имеются координаты каких-то особых точек объекта, то при изменении его положения координаты точек изменятся. А если мы соединим некоторые точки линиями, и измерим расстояния между ними? Длины отрезков к изменению положения уже инварианты, сколько ни смещай объект, длины не изменятся. Более того, они инвариантны даже к вращению! А если мы возьмем не длины отрезков, а углы между ними? Тогда описание объекта станет инвариантным к изменению размеров (помните из школы теорему, что углы подобных треугольников равны?). А с графов мы вообще можем по всякий «извращаться».  В общем, мы сделали первый шаг к тому, чтобы выделять на изображениях фичи и работать с ними. На этой оптимистичной ноте я закончу этот урок, до новых встреч. В заключении полный текст программы данного примера (который строит граф):  Программист