[Перевод] Использование Python для реализации алгоритма обрезки по шву

задняя часть Python алгоритм Программа перевода самородков

Обрезка по стыку — это новый способ обрезки изображения без потери важного содержимого изображения. Это часто называют кадрированием с учетом содержимого или перенаправлением изображения. Вы можете получить представление об алгоритме на этой фотографии:

Фото пользователя Unsplash Пьетро Де Гранди

становится следующим:

Как видите, очень важная часть изображения, лодка, сохранилась. Алгоритм удаляет некоторые скальные образования и воду (чтобы лодка выглядела ближе). Основной алгоритм может ссылаться на оригинальную статью Шая Авидана и Ариэля Шамира.Seam Carving for Content-Aware Image Resizing. В этом посте я покажу, как в основном реализовать алгоритм на Python.

резюме

Алгоритм работы следующий:

  1. Назначьте значение энергии каждому пикселю (энергия)
  2. Найдите 8 связанных областей пикселя с наименьшей энергией
  3. удалить все пиксели в этой области
  4. Повторяйте 1-3, пока не будет удалено нужное количество строк/столбцов.

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

Ниже приведены пакеты, которые код Python должен импортировать:

import sys

import numpy as np
from imageio import imread, imwrite
from scipy.ndimage.filters import convolve

# tqdm 并不是必需的,但它可以向我们展示一个漂亮的进度条
from tqdm import trange

энергетическая карта

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

Что это значит?IОт имени изображения этот формат говорит нам, что для каждого пикселя и каждого канала в изображении мы делаем следующие шаги:

  • Найдите частную производную оси x
  • Найдите частную производную оси Y
  • суммировать их абсолютные значения

Это значение энергии этого пикселя. Тогда возникает вопрос: «Как вычислить производную изображения?», в Википедии.Производные изображенияОн показал нам множество различных методов вычисления производных изображений. Мы будем использовать фильтр Собеля. Это основано на изображении на каждом каналесверточное ядро. Вот фильтры для двух разных ориентаций изображения:

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

def calc_energy(img):
    filter_du = np.array([
        [1.0, 2.0, 1.0],
        [0.0, 0.0, 0.0],
        [-1.0, -2.0, -1.0],
    ])
    # 将一个 2D 的滤波器转为 3D 的滤波器,为每个通道设置相同的滤波器:R,G,B
    filter_du = np.stack([filter_du] * 3, axis=2)

    filter_dv = np.array([
        [1.0, 0.0, -1.0],
        [2.0, 0.0, -2.0],
        [1.0, 0.0, -1.0],
    ])
    # 将一个 2D 的滤波器转为 3D 的滤波器,为每个通道设置相同的滤波器:R,G,B
    filter_dv = np.stack([filter_dv] * 3, axis=2)

    img = img.astype('float32')
    convolved = np.absolute(convolve(img, filter_du)) + np.absolute(convolve(img, filter_dv))

    # 我们将红绿色蓝三通道中的能量相加
    energy_map = convolved.sum(axis=2)

    return energy_map

После визуализации энергетической карты мы можем увидеть:

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

###  Найдите шов с минимальной энергией (шов)

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

Так как же найти эту линию на дороге? Оказывается, для решения этой проблемы можно использовать динамическое программирование!

Давайте создадимM2D-массив для хранения минимального значения энергии для каждого пикселя. Если вы не знакомы с динамическим программированием, это просто означает, что минимальная энергия во всех возможных швах от вершины изображения до этой точки равнаM[i,j]. Следовательно, последняя строка M будет содержать минимальную энергию от верха до низа изображения. Нам нужно вернуться назад, чтобы найти пиксели, которые существуют в этом шве, поэтому мы сохраним эти значения в файле с именемbacktrackв двумерном массиве.

def minimum_seam(img):
    r, c, _ = img.shape
    energy_map = calc_energy(img)

    M = energy_map.copy()
    backtrack = np.zeros_like(M, dtype=np.int)

    for i in range(1, r):
        for j in range(0, c):
            # 处理图像的左边缘,防止索引到 -1
            if j == 0:
                idx = np.argmin(M[i - 1, j:j + 2])
                backtrack[i, j] = idx + j
                min_energy = M[i - 1, idx + j]
            else:
                idx = np.argmin(M[i - 1, j - 1:j + 2])
                backtrack[i, j] = idx + j - 1
                min_energy = M[i - 1, idx + j - 1]

            M[i, j] += min_energy

    return M, backtrack

Удалить пиксели в швах с наименьшей энергией

Затем мы можем удалить пиксели в шве с наименьшей энергией и вернуть новое изображение:

def carve_column(img):
    r, c, _ = img.shape

    M, backtrack = minimum_seam(img)

    # 创建一个(r,c)矩阵,所有值都为 True
    # 我们将删除图像中矩阵里所有为 False 的对应的像素
    mask = np.ones((r, c), dtype=np.bool)

    # 找到 M 最后一行中最小元素的那一列的索引
    j = np.argmin(M[-1])

    for i in reversed(range(r)):
        # 标记这个像素之后需要删除
        mask[i, j] = False
        j = backtrack[i, j]

    # 因为图像是三通道的,我们将 mask 转为 3D 的
    mask = np.stack([mask] * 3, axis=2)

    # 删除 mask 中所有为 False 的位置所对应的像素,并将
    # 他们重新调整为新图像的尺寸
    img = img[mask].reshape((r, c - 1, 3))

    return img

Повторите для каждого столбца

Все основные работы сделаны! Теперь нам просто нужно бежатьcarve_columnпока мы не отбросим желаемое количество столбцов. Давайте создадим еще одинcrop_cфункция, изображение и коэффициент масштабирования в качестве входных данных. Если размер изображения (300 600) и мы хотим уменьшить его до (150 600),scale_cПросто установите его на 0,5.

def crop_c(img, scale_c):
    r, c, _ = img.shape
    new_c = int(scale_c * c)

    for i in trange(c - new_c): # 如果你不想用 tqdm,这里将 trange 改为 range
        img = carve_column(img)

    return img

сложить их вместе

Мы можем добавить основную функцию, чтобы код можно было вызывать из командной строки:

def main():
    scale = float(sys.argv[1])
    in_filename = sys.argv[2]
    out_filename = sys.argv[3]

    img = imread(in_filename)
    out = crop_c(img, scale)
    imwrite(out_filename, out)

if __name__ == '__main__':
    main()

Затем запустите этот код:

python carver.py 0.5 image.jpg cropped.jpg

cropped.jpg теперь должен отображать изображение, подобное этому:

![]https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/7/12/1648d13cb3f0ab58~tplv-t2oaga2asx-image.image)

Как следует обрабатывать линию?

Затем мы можем приступить к изучению того, как изменить наш цикл для обработки данных в другом направлении. Или... просто поверните изображение и запуститеcrop_c!

def crop_r(img, scale_r):
    img = np.rot90(img, 1, (0, 1))
    img = crop_c(img, scale_r)
    img = np.rot90(img, 3, (0, 1))
    return img

Добавьте этот код в функцию main, и теперь мы тоже можем обрезать линии!

def main():
    if len(sys.argv) != 5:
        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)
        sys.exit(1)

    which_axis = sys.argv[1]
    scale = float(sys.argv[2])
    in_filename = sys.argv[3]
    out_filename = sys.argv[4]

    img = imread(in_filename)

    if which_axis == 'r':
        out = crop_r(img, scale)
    elif which_axis == 'c':
        out = crop_c(img, scale)
    else:
        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)
        sys.exit(1)
    
    imwrite(out_filename, out)

Запустите код:

python carver.py r 0.5 image2.jpg cropped.jpg

Тогда мы можем поставить это изображение:

Photo by Brent Cox on Unsplash

становится таким:

Суммировать

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

Вот полная программа:

#!/usr/bin/env python

"""
Usage: python carver.py <r/c> <scale> <image_in> <image_out>
Copyright 2018 Karthik Karanth, MIT License
"""

import sys

from tqdm import trange
import numpy as np
from imageio import imread, imwrite
from scipy.ndimage.filters import convolve

def calc_energy(img):
    filter_du = np.array([
        [1.0, 2.0, 1.0],
        [0.0, 0.0, 0.0],
        [-1.0, -2.0, -1.0],
    ])
    # 将一个 2D 的滤波器转为 3D 的滤波器,为每个通道设置相同的滤波器:R,G,B
    filter_du = np.stack([filter_du] * 3, axis=2)

    filter_dv = np.array([
        [1.0, 0.0, -1.0],
        [2.0, 0.0, -2.0],
        [1.0, 0.0, -1.0],
    ])
    # 将一个 2D 的滤波器转为 3D 的滤波器,为每个通道设置相同的滤波器:R,G,B
    filter_dv = np.stack([filter_dv] * 3, axis=2)

    img = img.astype('float32')
    convolved = np.absolute(convolve(img, filter_du)) + np.absolute(convolve(img, filter_dv))

    # 我们将红绿色蓝三通道中的能量相加
    energy_map = convolved.sum(axis=2)

    return energy_map

def crop_c(img, scale_c):
    r, c, _ = img.shape
    new_c = int(scale_c * c)

    for i in trange(c - new_c):
        img = carve_column(img)

    return img

def crop_r(img, scale_r):
    img = np.rot90(img, 1, (0, 1))
    img = crop_c(img, scale_r)
    img = np.rot90(img, 3, (0, 1))
    return img

def carve_column(img):
    r, c, _ = img.shape

    M, backtrack = minimum_seam(img)
    mask = np.ones((r, c), dtype=np.bool)

    j = np.argmin(M[-1])
    for i in reversed(range(r)):
        mask[i, j] = False
        j = backtrack[i, j]

    mask = np.stack([mask] * 3, axis=2)
    img = img[mask].reshape((r, c - 1, 3))
    return img

def minimum_seam(img):
    r, c, _ = img.shape
    energy_map = calc_energy(img)

    M = energy_map.copy()
    backtrack = np.zeros_like(M, dtype=np.int)

    for i in range(1, r):
        for j in range(0, c):
            # 处理图像的左边缘,防止索引到 -1
            if j == 0:
                idx = np.argmin(M[i-1, j:j + 2])
                backtrack[i, j] = idx + j
                min_energy = M[i-1, idx + j]
            else:
                idx = np.argmin(M[i - 1, j - 1:j + 2])
                backtrack[i, j] = idx + j - 1
                min_energy = M[i - 1, idx + j - 1]

            M[i, j] += min_energy

    return M, backtrack

def main():
    if len(sys.argv) != 5:
        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)
        sys.exit(1)

    which_axis = sys.argv[1]
    scale = float(sys.argv[2])
    in_filename = sys.argv[3]
    out_filename = sys.argv[4]

    img = imread(in_filename)

    if which_axis == 'r':
        out = crop_r(img, scale)
    elif which_axis == 'c':
        out = crop_c(img, scale)
    else:
        print('usage: carver.py <r/c> <scale> <image_in> <image_out>', file=sys.stderr)
        sys.exit(1)
    
    imwrite(out_filename, out)

if __name__ == '__main__':
    main()

Изменить (5 мая 2018 г.):какЗаядлый пользователь реддитасказал, используяnumbaЧтобы ускорить выполнение ресурсоемких функций, вы можете легко добиться увеличения производительности в десятки раз. Чтобы испытать numba, просто используйте функциюcarve_columnиminimum_seamдобавить перед@numba.jit. Как следующее:

@numba.jit
def carve_column(img):

@numba.jit
def minimum_seam(img):

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


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.