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

Ниже в статье приведён подробный разбор кода для построения и визуализации таких деревьев, технические детали реализации и ссылка на демо, которое можно запустить прямо в браузере.
Формализация задачи
Определим дерево как объект, который строится по правилам, изложенным далее, начиная с единственного отрезка - ствола. У каждой ветки (включая ствол), кроме терминальных (оконечных, самых "тонких" веток) одинаковое число потомков. Длина потомка равна длине родителя, умноженной на некоторую константу, потомки равномерно распределены в некотором диапазоне углов. Еще одной константой определяется максимальная глубина рекурсии, то есть количество уровней потомков (дети, внуки, ...) у ствола.
Менее красочная, но при этом менее загромождённая и более понятная картинка, приведённая ниже, содержит все перечисленные компоненты: ветвление, уменьшение длины потомков, окончание рекурсии на некоторой глубине, равномерное распределение потомков внутри диапазона углов.

Для более живой и привлекательной отрисовки введём прибавление небольшой центрированной случайной величины к углу, под которым рисуется каждая ветка. Дрожание на видео в начале статьи вызвано именно этим. И на картинке выше можно увидеть, что ствол немного наклонён относительно вертикали.
Структура кода
Генерация и рисование деревьев построены на классе Branch, который представляет из себя совокупность перечисленных выше параметров ветки и списка веток-потомков.
За классом следует объявление ползунков (трекбаров?.. на базовой кафедре с трепетом относились к импортозамещению в терминологии) для регулировки параметров, с которыми будет генерироваться дерево:
cv2.createTrackbar('length', 'trackbars', 170, 300, nothing)

За созданием трекбаров следует вечный цикл (который, впрочем, прерывается нажатием кнопки q), в котором происходит создание дерева и его отрисовка.
Объект класс Branch, инициализация которого будет подробно разобрана ниже, создается с параметрами, полученными считыванием значений трекбаров в текущий момент времени.
tree = Branch (length_ = cv2.getTrackbarPos('length', 'trackbars'), ...
Корень дерева находится не внизу экрана, чтобы можно было полностью наблюдать деревья с большим разбросом углов у веток.
tree.draw (canvas, 500, 550, 2, tick)
Для создания эффекта затухания с уходом вправо (который можно интерпретировать как тень, третье измерение и возможно ещё что-то) изображение домножается на 0.95 и сдвигается вправо на 4 пикселя - значения, подобранные на глаз.
shadow = canvas.astype ("float") * 0.95
new_canvas = canvas_.copy ()
new_canvas [:, shift :, :] = shadow [:, : -shift, :]
canvas = new_canvas
Наконец, происходит вывод изображения на экран и запись очередного кадра в видео (которая была удалена из кода, потому что она использует рукописную библиотеку, которую нельзя просто так взять и поставить. Но если хочется записать видео, можно найти туториал по cv2.VideoWriter). Именно с помощью этой функции были сгенерированы все примеры для этой статьи - можно просто смотреть, какие деревья получаются при разных параметрах, а потом выбирать лучшие моменты из записанной полностью истории. За счёт сжатия и того, что кадры по сравнению с естественными изображениями слабо детализированы, видео получаются небольшого объёма.
cv2.imshow ("render", canvas)
out.write (canvas)
class Branch
Визуализация
Функция draw(img, x, y, depth, tick) принимает на вход изображение, в которое будет происходить рисование, абсолютные координаты корня текущего поддерева, глубину, на которой рисуется лист в виде окружности, и номер прохода главного цикла, который служит дискретным аналогом времени.
Найдём координаты конца текущей ветки:
x1 = int (x + self.length * math.cos (self.angle))
y1 = int (y + self.length * math.sin (self.angle))
Нарисуем линию толщиной в одну шестьдесят пятую от длины, но не меньше одного пикселя:
cv2.line (img, (x, y), (x1, y1), self.color, max (1, int(self.length / 65)))
Если глубина достигла нуля, нарисуем лист:
if (depth == 0):
cv2.circle (img, (int (x1), int (y1)), 3, ((10 + tick * 7) % 255 + 10, 40, 10), -1)
Рекурсивно вызовем отрисовку для всех детей:
for child in self.children:
child.draw (img, x1, y1, depth - 1, tick)
Создание объекта
Конструктор принимает все параметры, о которых шла речь выше, и ещё некоторые, а именно угол (дерево может вращать целиком) и цвет.
def __init__(self, length_, angle_, color_, angle_range_,
length_decrement_factor_, max_depth_, branching_factor_):
self.length = length_
self.angle = angle_ + float (np.random.randint (300) - 150) / 10000
self.color = color_
Если ветка является терминальной, то есть не имеет потомков, для разнообразия нарисуем её другим цветом.
if (max_depth_ < 1):
self.color = (color_[0] + 10, color_[0] + 30, color_[0] + 20)
self.children = []
Разберём отдельно случай с единственным потомком на ветку, чтобы ненароком не поделить на ноль.
if (max_depth_ > 0):
if (branching_factor_ > 1):
angle_step = angle_range_ / (branching_factor_ - 1)
else:
angle_step = angle_range_ / 2
Добавим в список детей текущей ветки количество веток, равное branching_factor с параметрами, зависящими от её праметров.
for i in range (branching_factor_):
Длина потомка равна длине предка, умноженной на числовой коэффициент.
self.add_child (length_ = int (self.length * length_decrement_factor_),
Угол потомка равен углу предка, к которому прибавлен угловой шаг, умноженный на номер потомка.
angle_ = - angle_range_ / 2 +
self.angle + i * angle_step,
Цвет, диапазон углов, коэффициент, с которым изменяется длина, и количество потомков остаются неизменными.
color_ = self.color,
angle_range_ = angle_range_,
length_decrement_factor_ = length_decrement_factor_,
Ограничение на глубину уменьшается на 1 для выхода из рекурсии.
max_depth_ = max_depth_ - 1,
branching_factor_ = branching_factor_)
Если потомок становится больше предка, древесная структура может перестать прослеживаться за счёт выхода далеко за пределы экрана.
Итоги, конкурс деревьев
Немного модифицированная версия кода, готовая к запуску прямо в браузере и прямо сейчас, лежит здесь. Построение чуть медленнее, чем в локальной версии, но все параметры регулировать можно.
Полный код проекта лежит в этом репозитории. Быстро работающая версия rendering_local.ipynb требует для работы интерпретатор питона и установленную библиотеку OpenCV.
Семинар с очень, очень подробным рассказом про то, как это работает, выложен здесь.
Если получилось построить красивое дерево, можно его заскринить или сохранить видос и отправить в чат. Потом устроим голосование и выложим самые-самые в наш инстаграм :)