Рубрики
Без рубрики

Изменение размера содержимого изображения размера в JavaScript

JavaScript Реализация так называемого алгоритма резки резьбы швования для изменений изображения и удаления объектов. Динамический подход программирования применяется для оптимизации времени изменений.

Автор оригинала: Oleksii Trekhleb.

Есть Интерактивная версия этого поста Доступно, где вы можете загружать и изменить размер пользовательских изображений.

TL; доктор

Там много замечательных статей, написанных о Алгоритм резьбы швования Уже, но я не мог противостоять искушению изучить этот элегантный, мощный и все же простой Алгоритм самостоятельно, и писать о моем личном опыте с ним. Еще один момент, который обратил мое внимание (как создатель JavaScript-алгоритмы репо) был тот факт, что Динамическое программирование (DP) Подход может быть плавно применен для его решения. И, если вы похожи на меня и еще в своем пути «Алгоритмы обучения», этот алгоритмический раствор может обогатить вашему личному арсеналу ДП.

Итак, с этой статьей я хочу сделать три вещи:

  1. Предоставить вам интерактивный Adver-Aware Resizer так что вы могли бы играть с изменением изменения ваших собственных изображений
  2. Объясните идею за Алгоритм резьбы швования
  3. Объясните Динамический подход к программированию Для реализации алгоритма (мы будем использовать для него Teamscript)

Изменение размера изображения содержимого

Изменение размера изображений содержимого Можно применяться, когда дело доходит до изменения пропорций изображения (то есть уменьшая ширину при сохранении высоты) и при терянии некоторых частей изображения не желательна. Выполнение простого масштабирования изображений в этом случае искажает объекты в нем. Чтобы сохранить пропорции объектов при изменении пропорций изображения, мы можем использовать Алгоритм резьбы швования это было введено Шай Авидан и Ариэль Шамир Отказ

Приведенный ниже пример показывает, как ширина исходного изображения была уменьшена на 50%, используя Увеличение размера содержимого размера (левое изображение) и Простое масштабирование (правильное изображение). В этом конкретном случае левое изображение выглядит более естественным, так как пропорции воздушных шаров сохранились.

Изменение размера изображения содержимого

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

JS Image Carver Demo

Нахождение шов с самой низкой энергией – это вычислительно дорогое задача (особенно для больших изображений). Чтобы сделать поиск шва быстрее Динамическое программирование Подход может быть применен (мы пройдемся через детали реализации ниже).

Удаление объектов

Важность каждого пикселя (так называемая энергия пикселей) рассчитывается на основе его цвета ( R , G , B , A ) Разница между двумя соседними пикселями. Теперь, если мы установим энергию пикселей на некоторое действительно низкий уровень искусственно (т.е. путем рисования маски сверху), алгоритм резьбы шва выполнит Удаление объекта Для нас бесплатно.

JS Image Carver Editoval Demo Demo

JS Image Carver Demo

Я создал JS Image Carver Web-App (а также открыть его на Github ), что вы можете использовать для игры с изменением размеров ваших пользовательских изображений.

Больше примеров

Вот еще несколько примеров того, как алгоритм справляется с более сложными фонами.

Горы на заднем плане сжаваются гладко без видимых швов.

Изменение размера демонстрации с более сложными фоном

То же самое касается океанских волн. Алгоритм сохранил волновую структуру без искажения серферов.

Изменение размера демонстрации с более сложными фоном

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

Пример, когда алгоритм не работает должным образом

Как работает алгоритмы вырезания шов

Представьте, что у нас есть 1000 х 500 px картинка, и мы хотим изменить его размер в 500 х 500 px Чтобы сделать его квадратом (скажем, квадратный соотношение лучше соответствует корму в Instagram). Мы могли бы захотеть настроить несколько Требования к процессу размера в таком случае:

  • Сохранить важные части изображения (то есть, если было 5 деревьев до размера, мы хотим также иметь 5 деревьев после размера).
  • Сохранить пропорции Из важных частей изображения (т.е. круга автомобильные колеса не должны быть сжаты на колеса эллипса)

Чтобы не изменять важные части изображения, мы можем найти Непрерывная последовательность пикселей (шов) , что идет сверху вниз и имеет самый низкий вклад в контент изображения (избегает важных частей), а затем удалить его. Удаление шва будет сокращать изображение на 1 пиксель. Затем мы повторим этот шаг, пока изображение не получит желаемую ширину.

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

Пиксели различий в цвете

Предполагая, что цвет пикселя представлен 4 Числа ( r – красный, g – зеленый, b – синий, a – альфа) Мы можем использовать следующую формулу для расчета разницы цвета (энергия пикселей):

Формула энергии пикселей

Где:

  • МенергияЭнергия (Важность) Средний пиксель ( [0..626] если закруглен)
  • ЛРКрасный Значение канала для левый пиксель ( [0..255] )
  • МистерКрасный Значение канала для Средний пиксель ( [0..255] )
  • RRКрасный Значение канала для правильно пиксель ( [0..255] )
  • LGЗеленый Значение канала для левый пиксель ( [0..255] )
  • и так далее…

В формуле выше мы опускаем канал Alpha (прозрачность), на данный момент, предполагая, что в изображении нет прозрачных пикселей. Позже мы будем использовать альфа-канал для маскировки и удаления объекта.

Пример расчета энергии пикселей

Теперь, так как мы знаем, как найти энергию одного пикселя, мы можем рассчитать, так называемые, Энергетическая карта который будет содержать энергии каждого пикселя изображения. На каждом этапе размера на этапе энергии карты должна быть пересчитана (по меньшей мере, частично, больше об этом ниже) и будет иметь одинаковый размер, что и изображение.

Например, на шаге 1-го размера у нас будет 1000 х 500 Изображение и A 1000 х 500 Энергетическая карта. На шаге 2-го размера мы удалим шов из изображения и пересчитаем карту энергии на основе нового сжатого изображения. Таким образом, мы получим 999 х 500 Изображение и A 999 х 500 Энергетическая карта.

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

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

ЭНЕС ЭНЕРГИЧЕСКИЙ СИРОК

Вот реальный пример энергии карты для демонстрации демонстрации, который вы видели выше (с горячими воздушными шарами).

Пример карты энергии

Вы можете играть с вашими пользовательскими изображениями и посмотреть, как будет выглядеть карта энергии в Интерактивная версия поста Отказ

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

Ищет шов

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

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

Пример карты энергии с шов

В приведенных выше примерах мы снижали ширину изображения. Аналогичный подход может быть принят для уменьшения высоты изображения. Нам нужно «вращать» подход, хотя:

  • Начните использовать верх и снизу пиксельные соседи (вместо покинули и правый те) для расчета энергии пикселей
  • При поиске шов нам нужно перейти от левый к правильно (вместо из up до Нижнее )

Реализация в типографии

Вы можете найти исходный код, а функции, упомянутые ниже в JS-образ-карвер Репозиторий.

Для реализации алгоритма мы будем использовать Tymdercript. Если вы хотите версию JavaScript, вы можете игнорировать (удалить) определения типа и их использование.

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

Изменение размера ширины содержимого (функция ввода)

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

// Type that describes the image size (width and height).
type ImageSize = { w: number, h: number };

// The coordinate of the pixel.
type Coordinate = { x: number, y: number };

// The seam is a sequence of pixels (coordinates).
type Seam = Coordinate[];

// Energy map is a 2D array that has the same width and height
// as the image the map is being calculated for.
type EnergyMap = number[][];

// Type that describes the image pixel's RGBA color.
type Color = [
  r: number, // Red
  g: number, // Green
  b: number, // Blue
  a: number, // Alpha (transparency)
] | Uint8ClampedArray;

На высоком уровне алгоритм состоит из следующих шагов:

  1. Рассчитать Энергетическая карта Для текущей версии изображения.
  2. Найти Шов С самой низкой энергией на основе карты энергии (именно здесь мы будем применять динамическое программирование).
  3. Удалить шов с самым низким энергетическим шов от изображения.
  4. Повторять пока ширина изображения не будет уменьшена до желаемого значения.
type ResizeImageWidthArgs = {
  img: ImageData, // Image data we want to resize.
  toWidth: number, // Final image width we want the image to shrink to.
};

type ResizeImageWidthResult = {
  img: ImageData, // Resized image data.
  size: ImageSize, // Resized image size (w x h).
};

// Performs the content-aware image width resizing using the seam carving method.
export const resizeImageWidth = (
  { img, toWidth }: ResizeImageWidthArgs,
): ResizeImageWidthResult => {
  // For performance reasons we want to avoid changing the img data array size.
  // Instead we'll just keep the record of the resized image width and height separately.
  const size: ImageSize = { w: img.width, h: img.height };

  // Calculating the number of pixels to remove.
  const pxToRemove = img.width - toWidth;
  if (pxToRemove < 0) {
    throw new Error('Upsizing is not supported for now');
  }

  let energyMap: EnergyMap | null = null;
  let seam: Seam | null = null;

  // Removing the lowest energy seams one by one.
  for (let i = 0; i < pxToRemove; i += 1) {
    // 1. Calculate the energy map for the current version of the image.
    energyMap = calculateEnergyMap(img, size);

    // 2. Find the seam with the lowest energy based on the energy map.
    seam = findLowEnergySeam(energyMap, size);

    // 3. Delete the seam with the lowest energy seam from the image.
    deleteSeam(img, seam, size);

    // Reduce the image width, and continue iterations.
    size.w -= 1;
  }

  // Returning the resized image and its final size.
  // The img is actually a reference to the ImageData, so technically
  // the caller of the function already has this pointer. But let's
  // still return it for better code readability.
  return { img, size };
};

Изображение, которое должно быть изменено изменением, передается на функцию в Imagedata формат. Вы можете нарисовать изображение на холсте, а затем извлечь imagedata из холста, как это:

const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, imgWidth, imgHeight);

Способ загрузки и рисования изображений в JavaScript отсутствует в рамках этой статьи, но вы можете найти полный исходный код того, как он может быть сделан с использованием реагирования в JS-образ-карвер репо.

Давайте сломаем каждый шаг ony Be One и реализуйте CalculateEnerGyMap () , findlowenergaseam () и deleteseam () Функции.

Расчет энергии пикселей

Здесь мы применяем формулу разницы цвета, описанную выше. Для левых и правых границ (когда нет левых или правых соседей), мы игнорируем соседей и не принимайте их во внимание во время расчета энергетики.

// Calculates the energy of a pixel.
const getPixelEnergy = (left: Color | null, middle: Color, right: Color | null): number => {
  // Middle pixel is the pixel we're calculating the energy for.
  const [mR, mG, mB] = middle;

  // Energy from the left pixel (if it exists).
  let lEnergy = 0;
  if (left) {
    const [lR, lG, lB] = left;
    lEnergy = (lR - mR) ** 2 + (lG - mG) ** 2 + (lB - mB) ** 2;
  }

  // Energy from the right pixel (if it exists).
  let rEnergy = 0;
  if (right) {
    const [rR, rG, rB] = right;
    rEnergy = (rR - mR) ** 2 + (rG - mG) ** 2 + (rB - mB) ** 2;
  }

  // Resulting pixel energy.
  return Math.sqrt(lEnergy + rEnergy);
};

Расчет энергии

Изображение, с которым мы работаем, имеет Imagedata формат. Это означает, что все пиксели (и их цвета) хранятся в квартире ( 1D ) Uint8ClampedArray множество. Для чтения читаемости давайте представим пару функций вспомогательных функций, которые позволят нам работать с массивом UINT8ClampedArray, как с 2D вместо этого матрица.

// Helper function that returns the color of the pixel.
const getPixel = (img: ImageData, { x, y }: Coordinate): Color => {
  // The ImageData data array is a flat 1D array.
  // Thus we need to convert x and y coordinates to the linear index.
  const i = y * img.width + x;
  const cellsPerColor = 4; // RGBA
  // For better efficiency, instead of creating a new sub-array we return
  // a pointer to the part of the ImageData array.
  return img.data.subarray(i * cellsPerColor, i * cellsPerColor + cellsPerColor);
};

// Helper function that sets the color of the pixel.
const setPixel = (img: ImageData, { x, y }: Coordinate, color: Color): void => {
  // The ImageData data array is a flat 1D array.
  // Thus we need to convert x and y coordinates to the linear index.
  const i = y * img.width + x;
  const cellsPerColor = 4; // RGBA
  img.data.set(color, i * cellsPerColor);
};

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

// Helper function that creates a matrix (2D array) of specific
// size (w x h) and fills it with specified value.
const matrix = (w: number, h: number, filler: T): T[][] => {
  return new Array(h)
    .fill(null)
    .map(() => {
      return new Array(w).fill(filler);
    });
};

// Calculates the energy of each pixel of the image.
const calculateEnergyMap = (img: ImageData, { w, h }: ImageSize): EnergyMap => {
  // Create an empty energy map where each pixel has infinitely high energy.
  // We will update the energy of each pixel.
  const energyMap: number[][] = matrix(w, h, Infinity);
  for (let y = 0; y < h; y += 1) {
    for (let x = 0; x < w; x += 1) {
      // Left pixel might not exist if we're on the very left edge of the image.
      const left = (x - 1) >= 0 ? getPixel(img, { x: x - 1, y }) : null;
      // The color of the middle pixel that we're calculating the energy for.
      const middle = getPixel(img, { x, y });
      // Right pixel might not exist if we're on the very right edge of the image.
      const right = (x + 1) < w ? getPixel(img, { x: x + 1, y }) : null;
      energyMap[y][x] = getPixelEnergy(left, middle, right);
    }
  }
  return energyMap;
};

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

Нахождение шов с самой низкой энергией (динамический подход программирования)

Я описал некоторые динамические основы программирования в Динамическое программирование против Divide-and-Proquer Статья раньше. Существует пример DP, основанный на минимальной редактированной задаче расстояния. Вы можете проверить это, чтобы получить еще более контекст.

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

Наивный подход

Наивный подход будет проверять все возможные пути один за другим.

Наивный подход

Сдвиньте сверху вниз, для каждого пикселя у нас есть 3 варианта (↙︎ пойти вниз – влево, ↓ Снижать вниз, ↘︎ идти вниз – справа). Это дает нам временную сложность O (w * 3 ^ h) или просто O (3 ^ ч) , где W и H это ширина и высота изображения. Этот подход выглядит медленно.

Жадный подход

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

Жадный подход

Этот подход дает не худшее решение, но он не может гарантировать, что мы найдем лучшее доступное решение. На изображении выше вы можете увидеть, как жадный подход выбрал 5 вместо 10 Сначала пропустил цепочку оптимальных пикселей.

Хорошая часть об этом подходе состоит в том, что это быстро, и у него есть временная сложность O (w + h) , где W и H это ширина и высота изображения. В этом случае стоимость скорости – это низкое качество размера. Нам нужно найти минимальное значение в первом ряду (пересекающих W клетки), а затем мы исследуем только 3 соседних пикселя для каждой строки (пересекающие h строки).

Динамический подход программирования

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

Повторные проблемы

В приведенном выше примере вы видите, что для первых двух швов мы повторно используем энергию более короткого шва (у которой есть энергия 235 ). Вместо того, чтобы сделать только одну операцию 235 + 70 Для расчета энергии 2-го шов мы выполняем четыре операции (5 + 0 + 80 + 150) + 70 Отказ

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

Итак, мы можем Сохраните энергию текущего шва На конкретном пикселе в дополнительном Sowersenergies Таблица, чтобы сделать его повторно использовать для расчета следующих швов быстрее (The Seassenergies таблица будет иметь тот же размер, что и карта энергии и сам изображение).

Давайте также имеем в виду, что для одного конкретного пикселя на изображении (то есть внизу слева), у нас может быть Несколько значения предыдущих швов энергий.

Какой шов выбрать

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

Пример энергии швов

В общем, у нас есть три возможных предыдущих, кажется, выбирают из:

Три варианта на выбор

Вы можете подумать об этом таким образом:

  • Клетки [1] [x] : содержит самую низкую возможную энергию шва, который начинается где-то на ряд [0] [?] и заканчивается на ячейке [1] [X]
  • Текущая клетка [2] [3] : содержит самую низкую возможную энергию шва, который начинается где-то на ряд [0] [?] и заканчивается на ячейке [2] [3] Отказ Рассчитать его нужно подвести энергию текущего пикселя [2] [3] (с карты энергии) с min (seam_energy_1_2, seam_energy_1_3, seam_energy_1_4)

Если мы заполним Sowersenergies Таблица полностью, то минимальное число в самых низких рядах будет самым низким возможным энергией шва.

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

Швы Energies Map Traversal

После заполнения Sowersenergies Таблица мы можем видеть, что самый низкий энергетический пиксель имеет энергию 50 Отказ Для удобства, во время Sowersenergies Создание для каждого пикселя, мы можем сэкономить не только энергию шва, но и координаты предыдущего минимума энергии. Это даст нам возможность восстановить путь шва сверху вниз легко.

Сложность времени подхода DP будет O (w * h) , где W и H это ширина и высота изображения. Нам нужно рассчитать энергии для каждый пиксель изображения.

Вот пример того, как эта логика может быть реализована:

// The metadata for the pixels in the seam.
type SeamPixelMeta = {
  energy: number, // The energy of the pixel.
  coordinate: Coordinate, // The coordinate of the pixel.
  previous: Coordinate | null, // The previous pixel in a seam.
};

// Finds the seam (the sequence of pixels from top to bottom) that has the
// lowest resulting energy using the Dynamic Programming approach.
const findLowEnergySeam = (energyMap: EnergyMap, { w, h }: ImageSize): Seam => {
  // The 2D array of the size of w and h, where each pixel contains the
  // seam metadata (pixel energy, pixel coordinate and previous pixel from
  // the lowest energy seam at this point).
  const seamsEnergies: (SeamPixelMeta | null)[][] = matrix(w, h, null);

  // Populate the first row of the map by just copying the energies
  // from the energy map.
  for (let x = 0; x < w; x += 1) {
    const y = 0;
    seamsEnergies[y][x] = {
      energy: energyMap[y][x],
      coordinate: { x, y },
      previous: null,
    };
  }

  // Populate the rest of the rows.
  for (let y = 1; y < h; y += 1) {
    for (let x = 0; x < w; x += 1) {
      // Find the top adjacent cell with minimum energy.
      // This cell would be the tail of a seam with lowest energy at this point.
      // It doesn't mean that this seam (path) has lowest energy globally.
      // Instead, it means that we found a path with the lowest energy that may lead
      // us to the current pixel with the coordinates x and y.
      let minPrevEnergy = Infinity;
      let minPrevX: number = x;
      for (let i = (x - 1); i <= (x + 1); i += 1) {
        if (i >= 0 && i < w && seamsEnergies[y - 1][i].energy < minPrevEnergy) {
          minPrevEnergy = seamsEnergies[y - 1][i].energy;
          minPrevX = i;
        }
      }

      // Update the current cell.
      seamsEnergies[y][x] = {
        energy: minPrevEnergy + energyMap[y][x],
        coordinate: { x, y },
        previous: { x: minPrevX, y: y - 1 },
      };
    }
  }

  // Find where the minimum energy seam ends.
  // We need to find the tail of the lowest energy seam to start
  // traversing it from its tail to its head (from the bottom to the top).
  let lastMinCoordinate: Coordinate | null = null;
  let minSeamEnergy = Infinity;
  for (let x = 0; x < w; x += 1) {
    const y = h - 1;
    if (seamsEnergies[y][x].energy < minSeamEnergy) {
      minSeamEnergy = seamsEnergies[y][x].energy;
      lastMinCoordinate = { x, y };
    }
  }

  // Find the lowest energy energy seam.
  // Once we know where the tail is we may traverse and assemble the lowest
  // energy seam based on the "previous" value of the seam pixel metadata.
  const seam: Seam = [];
  if (!lastMinCoordinate) {
    return seam;
  }

  const { x: lastMinX, y: lastMinY } = lastMinCoordinate;

  // Adding new pixel to the seam path one by one until we reach the top.
  let currentSeam = seamsEnergies[lastMinY][lastMinX];
  while (currentSeam) {
    seam.push(currentSeam.coordinate);
    const prevMinCoordinates = currentSeam.previous;
    if (!prevMinCoordinates) {
      currentSeam = null;
    } else {
      const { x: prevMinX, y: prevMinY } = prevMinCoordinates;
      currentSeam = seamsEnergies[prevMinY][prevMinX];
    }
  }

  return seam;
};

Удаление шов с самой низкой энергией

Как только мы нашли самый низкий энергетический шов, нам нужно удалить (чтобы вырезать) пиксели, которые образуют его из изображения. Удаление происходит путем смещения пикселей справа от шов 1px Слева. Для причин производительности мы на самом деле не удалите последние столбцы. Вместо этого компонент рендеринга будет просто игнорировать часть изображения, который лежит за пределами ширины изображения изменений.

Удаление шва
// Deletes the seam from the image data.
// We delete the pixel in each row and then shift the rest of the row pixels to the left.
const deleteSeam = (img: ImageData, seam: Seam, { w }: ImageSize): void => {
  seam.forEach(({ x: seamX, y: seamY }: Coordinate) => {
    for (let x = seamX; x < (w - 1); x += 1) {
      const nextPixel = getPixel(img, { x: x + 1, y: seamY });
      setPixel(img, { x, y: seamY }, nextPixel);
    }
  });
};

Удаление объектов

Алгоритм резки шов пытается удалить швы, которые состоят из пикселей с низкой энергией. Мы могли бы использовать этот факт и присвоение низкой энергии в несколько пикселей вручную (то есть. Рисуясь на изображение и замаскиваем некоторые области), мы могли бы сделать алгоритм резьбы швования, чтобы сделать Удаление объектов Для нас бесплатно.

В настоящее время в GetPixelEnergy () Функция мы использовали только R , G , B Цветные каналы для расчета энергии пикселей. Но есть также А (альфа, прозрачность) параметр цвета, который мы еще не использовали. Мы можем использовать канал прозрачности, чтобы сказать алгоритму, что прозрачные пиксели являются пикселями, которые мы хотим удалить. Вы можете проверить Исходный код энергетической функции это принимает прозрачность во внимание.

Вот как алгоритм работает для удаления объекта.

JS Image Carver Editoval Demo Demo

Проблемы и что дальше

JS Image Carver Веб-приложение далеко не является готовым резопазом, конечно. Его основная цель состояла в том, чтобы экспериментировать с алгоритмом резьбы швования. Таким образом, план на будущее – продолжить эксперименты.

оригинальная бумага Описывает, как алгоритм резьбы шва может быть использован не только для нижней планки, но и для Увеличение изображений Отказ Высококалинг, в свою очередь, может быть использован для Выделите изображение обратно к исходной ширине после удаления объектов Отказ

Еще одна интересная область экспериментов может заключаться в том, чтобы сделать алгоритм работать в в режиме реального времени Отказ

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

Итак, удачи с вашими собственными экспериментами!