Узнайте, как написать скрипт на Python для создания фильтра на основе дерева квадрантов для стилизации фотографий.

Так недавно я обнаружил проект Майкла Фоглемана под названием Quadtree Art. Это вдохновило меня попробовать написать свою версию проекта. Это то, о чем я расскажу в этой статье, как реализовать свою собственную художественную программу Quadtree, как я сделал здесь: github.com/ribab/quadart

Выше - сгенерированное изображение, которое я сделал из изображения яблока, которое я нашел на freepik.com с помощью kstudio. Оригинал выглядит так:

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

Чтобы проиллюстрировать работу алгоритма, я реализовал функцию максимальной рекурсии в QuadArt, создал 10 различных изображений с разной глубиной рекурсии с помощью этой команды оболочки: for i in {1..10}; do ./quadart.py apple.jpg -o r-out/apple-r$i.jpg -m $i --thresh 25; done, а затем сгенерировал PNG с помощью ImageMagick с помощью команды convert -delay 40 -loop 0 *.jpg apple-r.gif. GIF-изображение ниже, демонстрирующее магию квадратов в действии.

Проще говоря, алгоритм QuadArt

Хотя моя программа QuadArt занимает 181 строку кода, реальный рекурсивный алгоритм, используемый для создания QuadArt, можно описать всего в 8 строках.

class QuadArt:
  ...
1 def recursive_draw(self, x, y, w, h):
      '''Draw the QuadArt recursively
      '''
2     if self.too_many_colors(int(x), int(y), int(w), int(h)):
3         self.recursive_draw(x,         y,         w/2.0, h/2.0)
4         self.recursive_draw(x + w/2.0, y,         w/2.0, h/2.0)
5         self.recursive_draw(x,         y + h/2.0, w/2.0, h/2.0)
6         self.recursive_draw(x + w/2.0, y + h/2.0, w/2.0, h/2.0)
7     else:
8         self.draw_avg(x, y, w, h)

Вышеупомянутый алгоритм взят прямо из моего кода. class QuadArt - это класс, содержащий imageio данные изображения, wand холст для рисования и порог стандартного отклонения. x, y, w, h передаются в функцию, чтобы указать положение x, y левого верхнего угла анализируемого в данный момент фрагмента изображения, а также его ширину и высоту.

Отладка медленной генерации QuadArt

Первоначально я реализовал всю программу QuadArt с помощью модуля Python Wand, который использует ImageMagick под капотом. Эта библиотека прекрасно отображает круги. После написания кода через мой первый этап реализации фотофильтра на основе дерева квадратов я столкнулся с проблемой, когда код обрабатывался слишком долго. Как оказалось, проверка цвета каждого пикселя с помощью Wand занимает слишком много времени для вычисления стандартного отклонения, а у Wand не было встроенной функции для выполнения такого рода анализа. Кроме того, мне сложно определить, завис ли мой код, когда на экране ничего не отображается.

Чтобы определить, прогрессирует ли мой код, мне нужна была какая-то панель загрузки. Однако загружать полосы намного проще с итеративным алгоритмом, когда вы точно знаете, сколько итераций необходимо, чтобы алгоритм завершил работу. Я знаю, что с рекурсивным алгоритмом, основанным на четырехугольном дереве, рекурсивная глубина 1 будет выполняться не более 4 раз, а глубина 2 будет выполняться не более 16 раз и так далее. Поэтому, учитывая эту идею, я реализовал дополнение к алгоритму для отображения полосы загрузки в терминале во время выполнения программы. Эта полоса загрузки отслеживает, сколько раз рекурсивный алгоритм выполняется на глубине 3.

Чтобы функция панели загрузки отслеживала прогресс recursive_draw(), мне нужно было только отслеживать точки выхода и отслеживать текущую глубину рекурсии. Два типа точек выхода - это когда recursive_draw() либо повторяется дальше, либо нет. Вот функция recursive_draw(), измененная для вызова loading_bar():

def recursive_draw(self, x, y, w, h):
    '''Draw the QuadArt recursively
    '''
    if self.too_many_colors(int(x), int(y), int(w), int(h)):
        self.recurse_depth += 1

        self.recursive_draw(x,         y,         w/2.0, h/2.0)
        self.recursive_draw(x + w/2.0, y,         w/2.0, h/2.0)
        self.recursive_draw(x,         y + h/2.0, w/2.0, h/2.0)
        self.recursive_draw(x + w/2.0, y + h/2.0, w/2.0, h/2.0)

        self.recurse_depth -= 1

        if self.recurse_depth == 3:
            loading_bar(self.recurse_depth)
    else:
        self.draw_avg(x, y, w, h)

        loading_bar(self.recurse_depth)

loading_bar() имеет логику для расчета прогресса только при глубине ‹= 3, но мне все еще нужно было проверить, равно ли текущий self.recurse_depth 3 в 1-й точке выхода recursive_draw(), иначе будут избыточные вызовы loading_bar() из-за рекурсии.

Вот как выглядит loading_bar()

def loading_bar(recurse_depth):
    global load_progress
    global start_time
    load_depth=3
    recursion_spread=4
    try:
        load_progress
        start_time
    except:
        load_progress = 0
        start_time = time.time()
        print('[' + ' '*(recursion_spread**load_depth) + ']\r', end='')
    if recurse_depth <= load_depth:
        load_progress += recursion_spread**(load_depth - recurse_depth)
        cur_time = time.time()
        time_left = recursion_spread**load_depth*(cur_time - start_time)/load_progress \
                  - cur_time + start_time
        print('[' + '='*load_progress \
                  + ' '*(recursion_spread**load_depth - load_progress) \
                  + '] ' \
                  + 'time left: {} secs'.format(int(time_left)).ljust(19) \
                  + '\r', end='')

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

Анализ изображений с помощью imageio и numpy

Для порога recursive_draw() о том, следует ли разделить на несколько квадрантов, функция too_many_colors() вычисляет стандартное отклонение красного, зеленого и синего цветов и возвращает True, если стандартное отклонение превышает пороговое значение. Для поколения QuadArt, я считаю, что хороший порог составляет около 25 STD, иначе изображение станет либо слишком пиксельным, либо слишком мелкозернистым. Библиотека анализа изображений python imageio идеально подходит для такого рода анализа, поскольку она подключается прямо к numpy для быстрых статистических вычислений.

Моя первоначальная настройка для анализа изображений с помощью imageio и numpy выглядит следующим образом:

  1. Импортировать imageio и numpy
import imageio
import numpy as np

2. Считайте изображение с помощью imageio (имя файла - это имя изображения, которое мы анализируем)

img = imageio.imread(filename)

3. Выберите часть изображения, которую мы анализируем. Эффективная обрезка img. «Влево», «вправо», «вверх» и «вниз» указывают, где обрезать img.

self.img = self.img[up:down,left:right]

4. Найдите ширину и высоту изображения.

input_width = img.shape[1]
input_height = img.shape[0]

5. Убедитесь, что изображение имеет квадратную форму, вычтя разницу между длинной стороной и меньшей стороной.

if input_width < input_height:
    difference = input_height - input_width
    subtract_top = int(difference/2)
    subtract_bot = difference - subtract_top
    img = img[subtract_top:-subtract_bot,:]
elif input_height < input_width:
    difference = input_width - input_height
    subtract_left = int(difference/2)
    subtract_right = difference - subtract_left
    img = img[:,subtract_left:-subtract_right]

6. Теперь объект imageio «img» можно использовать для вычисления стандартного отклонения следующим образом:

# Selecting colors
red = img[:,:,0]
green = img[:,:,1]
blue = img[:,:,2]
# Calculating averages from colors
red_avg = np.average(red)
green_avg = np.average(green)
blue_avg = np.average(blue)
# Calculating standard deviations from colors
red_std = np.std(red)
green_std = np.std(green)
blue_std = np.std(blue)

Вот так моя программа QuadArt вычисляет, может ли функция recursive_draw() рекурсировать дальше из-за большого отклонения цвета. Взгляните на too_many_colors()

class QuadArt:
    ...
    def too_many_colors(self, x, y, w, h):
        if w * self.output_scale <= 2 or w <= 2:
            return False
        img = self.img[y:y+h,x:x+w]
        red = img[:,:,0]
        green = img[:,:,1]
        blue = img[:,:,2]

        red_avg = np.average(red)
        green_avg = np.average(green)
        blue_avg = np.average(blue)

        if red_avg >= 254 and green_avg >= 254 and blue_avg >= 254:
            return False

        if 255 - red_avg < self.std_thresh and 255 - green_avg < self.std_thresh \
                                           and 255 - blue_avg < self.std_thresh:
            return True

        red_std = np.std(red)
        if red_std > self.std_thresh:
            return True

        green_std = np.std(green)
        if green_std > self.std_thresh:
            return True

        blue_std = np.std(blue)
        if blue_std > self.std_thresh:
            return True

        return False

Вышеупомянутая функция делает это:

  1. Выбирает цвета
  2. Вычисляет средние по цветам
  3. Возвращает False сразу, если среднее значение довольно близко к белому
  4. Вычисляет стандартные отклонения от цветов
  5. Возвращает True (для дальнейшей рекурсии), если стандартное отклонение превышает пороговое значение для любого из цветов.
  6. В противном случае возвращает False

Наконец отображение кругов

Теперь перейдем к самому простому: отображение кругов в wand.

Моя стратегия применения фильтра изображений - построить получившееся изображение из пустого холста.

Это шаблон, как рисовать вещи с помощью палочки.

# Import Wand
from wand.image import Image
from wand.display import Display
from wand.color import Color
from wand.drawing import Drawing

# Set up canvas to draw on
canvas = Image(width = output_size,
               height = output_size,
               background = Color('white'))
canvas.format = 'png'
draw = Drawing()

# Draw circles and rectangles and anything else here
draw.fill_color = Color('rgb(%s,%s,%s)' % (red, green, blue))
draw.circle((x_center, y_center), (x_edge, y_edge))
draw.rectangle(x, y, x + w, y + h)

# Write drawing to the canvas
draw(canvas)

# If you want to display image to the screen
display(canvas)

# If you want to save image to a file
canvas.save(filename='output.png')

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

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

output_scale = float(output_size) / input_width

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

class QuadArt:
    ...
    def draw_avg(self, x, y, w, h):
        avg_color = self.get_color(int(x), int(y), int(w), int(h))
        self.draw_in_box(avg_color, x, y, w, h)

Функция get_color() сначала захватывает обрезанный участок входного изображения (в формате imageio), а затем вычисляет средние значения красного, зеленого и синего в этом обрезанном участке, а затем создает объект wand.color.Color на основе вычисленных средних цветов.

class QuadArt:
    ...
    def get_color(self, x, y, w, h):
        img = self.img[y : y + h,
                       x : x + w]
        red = np.average(img[:,:,0])
        green = np.average(img[:,:,1])
        blue = np.average(img[:,:,2])
        color = Color('rgb(%s,%s,%s)' % (red, green, blue))
        return color

Функция draw_in_box() рисует либо круг, либо квадрат внутри определенного прямоугольника, который представляет собой квадрант, рассчитанный ранее too_many_colors(), чтобы иметь достаточно низкое отклонение. Перед рисованием на холсте координаты вместе с шириной и высотой умножаются на output_scale. И цвет заливки wand.drawing устанавливается равным ранее вычисленному среднему цвету. Затем на полотно рисуется круг или квадрат.

class QuadArt:
    ...
    def draw_in_box(self, color, x, y, w, h):
        x *= self.output_scale
        y *= self.output_scale
        w *= self.output_scale
        h *= self.output_scale

        self.draw.fill_color = color

        if self.draw_type == 'circle':
            self.draw.circle((int(x + w/2.0), int(y + h/2.0)),
                             (int(x + w/2.0), int(y)))
        else:
            self.draw.rectangle(x, y, x + w, y + h)

Вот и все! Вот как я реализовал Quadtree Photo Stylizer, и как вы можете реализовать то же самое или вдохновиться и создать свой собственный алгоритм стилизации ваших фотографий.

Вы можете просмотреть весь код здесь: github.com/ribab/quadart/blob/master/quadart.py

Первоначально опубликовано на http://www.codingwithricky.com.