- Оригинальный адрес:Implementing Seam Carving with Python
- Оригинальный автор:Karthik Karanth
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:caoyi0905
- Корректор:yqian1991
Обрезка по стыку — это новый способ обрезки изображения без потери важного содержимого изображения. Это часто называют кадрированием с учетом содержимого или перенаправлением изображения. Вы можете получить представление об алгоритме на этой фотографии:
Фото пользователя Unsplash Пьетро Де Гранди
становится следующим:
Как видите, очень важная часть изображения, лодка, сохранилась. Алгоритм удаляет некоторые скальные образования и воду (чтобы лодка выглядела ближе). Основной алгоритм может ссылаться на оригинальную статью Шая Авидана и Ариэля Шамира.Seam Carving for Content-Aware Image Resizing. В этом посте я покажу, как в основном реализовать алгоритм на Python.
резюме
Алгоритм работы следующий:
- Назначьте значение энергии каждому пикселю (энергия)
- Найдите 8 связанных областей пикселя с наименьшей энергией
- удалить все пиксели в этой области
- Повторяйте 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-подключена: это означает, что каждый пиксель в линии может касаться следующего пикселя в линии через край или угол. Например, это красная линия на изображении ниже:
Так как же найти эту линию на дороге? Оказывается, для решения этой проблемы можно использовать динамическое программирование!
Давайте создадимM
2D-массив для хранения минимального значения энергии для каждого пикселя. Если вы не знакомы с динамическим программированием, это просто означает, что минимальная энергия во всех возможных швах от вершины изображения до этой точки равна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,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.