В этом блоге я собираюсь объяснить, как я создал скрипт Python для «фанкинга» изображений с помощью кода Python. Программа достаточно быстра и работает даже с видео в реальном времени (без графического процессора)! Так:

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

Как это работает

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

def funkify(self, img):
    
    edges = self.edge_mask(img)
    blur = cv2.GaussianBlur(img, (self.color_blur_val, self.color_blur_val), 
                            sigmaX=0, sigmaY=0)
    indices = self.pick_color(blur.reshape((-1, 3)), 
                              self.lightness, self.n_colors)
    recolored = np.uint8(self.colors[indices].reshape(blur.shape))
    cartoon = cv2.bitwise_and(recolored, recolored, mask=edges)

    return cartoon

Теперь я разберу это построчно на примере этого изображения.

Шаг 1: толстые края

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

Цель утолщения краев — сделать их похожими на чернильные линии в мультфильме или аниме. Получение ребер осуществляется первой строкой кода.

edges = self.edge_mask(img)

Функция Edge_mask() вызывает следующий код.

def edge_mask(self, img):
        # get the edges of the image
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        gray_blur = cv2.GaussianBlur(gray, 
                                    (self.edge_blur_val, self.edge_blur_val),
                                     -1)
        edges = cv2.adaptiveThreshold(gray_blur, 255, 
                                      cv2.ADAPTIVE_THRESH_MEAN_C, 
                                      cv2.THRESH_BINARY, 
                                      self.block_size, 
                                      2)
        return edges

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

Третья линия – основная часть. Адаптивное определение порога — это функция бинарного определения порога. Это означает, что он классифицирует каждый пиксель изображения либо как черный (0), либо как белый (1). Существует множество алгоритмов определения двоичного порога. Уникальность адаптивной пороговой обработки заключается в том, что она классифицирует каждый пиксель на основе интенсивности соседних пикселей, а не всего изображения. Это позволяет ему работать лучше в различных условиях освещения.

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

Шаг 2: перекрашивание изображения

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

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

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

Код перекраски в основной функции состоял из этих трех строк:

blur = cv2.GaussianBlur(img, (self.color_blur_val, self.color_blur_val), 
                         sigmaX=0, sigmaY=0)
indices = self.pick_color(blur.reshape((-1, 3)), 
                          self.luminance, self.n_colors)
recolored = np.uint8(self.colors[indices].reshape(blur.shape))

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

Вторая линия – самая важная. Функция pick_color() вычисляет цвет для каждого пикселя изображения. Он запускает этот код:

def pick_color(self, img, color_lums, n_colors):
    # reassign pixel colors based on luminance
    
    # get luminance of pixels
    lum_mult = [0.114, 0.587, 0.299]
    img_lum = np.sum(np.multiply(img, lum_mult), axis=1)
    
    # create list of conditions for each color
    condlist = []
    choicelist = []
    for i in range(n_colors):
        choicelist.append(i)
        if i < n_colors-1:
            condlist.append(img_lum < (color_lums[i]+color_lums[i+1])/2)
        else:
            condlist.append(img_lum > (color_lums[i]+color_lums[i-1])/2)
    
    # get index of new color for each pixel
    inds = np.select(condlist, choicelist)
    
    return inds

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

Затем создается список условий для определения того, какой цвет выбрать на основе яркостей. Создание этого списка условий позволяет функции адаптироваться к любому количеству цветов, которое хочет использовать пользователь, вместо жесткого кодирования заданного числа. Функция np.select() используется для фактического выбора из этого списка. По сути, это серия «если-операторов» для каждого пикселя, например:

# e.g. for 3 colors
if pix_lum < (color_lums[0] + color_lums[1])/2:
    pix_ind = 0
else if pix_lum < (color_lums[1] + color_lums[2])/2:
    pix_ind = 1
else:
    pix_ind = 2

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

Результат этого процесса выглядит следующим образом.

Шаг 3: объединение

Заключительная часть основной функции — объединить перекрашенное изображение и толстые края.

cartoon = cv2.bitwise_and(recolored, recolored, mask=edges)

Окончательный результат:

Выглядит довольно круто, правда?!

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

Надеюсь, вам понравился этот пост. Я настоятельно рекомендую вам пойти дальше и попробовать это на себе здесь!