Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Делаем свои собственные фильтры изображений

Оригинал: Making Your Own Image Filters
Автор: Cate Huston
Дата публикации: Wed 13 April 2016
Перевод: Н.Ромоданов
Дата перевода: май 2016 г.

Делаем фильтры самостоятельно

Фильтры RGB

Перед тем, как приступить к написанию более сложной обработки пикселей, мы можем начать с короткой тренировки, которая поможет нам почувствовать себя более комфортно при обработке пикселей. Мы создадим стандартные (красный, зеленый, синий) цветные фильтры, которые позволят нам создать тот же самый эффект, как если бы поместили цветную пластинку перед объективом камеры, позволяя проходить через неее только свету с достаточным количеством красной (зеленой, синей) компоненты.

Применяя различные фильтры к изображению, показанному на рис.5 (сделано во время весеннего путеществия во Франкфурт), мы получаем почти такие же изображения, как изображения в разные сезоны. Помните картинки с четырьмя сезонами, какими мы представляли их себе раньше?) Посмотрите, насколько гораздо более зеленым становится дерево, когда применяется красный фильтр.

Рис.5. Четыре (искусственно созданные) времени года во Франкфурте

Как мы это делаем?

  • Настраиваем фильтр. (Вы можете комбинировать красный, зеленый и синий фильтры, как в изображении, приведенном ранее; я не привожу примеров, поскольку эффект и так очевиден).
  • Для каждого пикселя изображения проверяем его значение RGB.
  • Если значение красной составляющей меньше, чем значение красного фильтра, то оно устанавливается равным нулю.
  • Если значение зеленой составляющей меньше, чем значение зеленого фильтра, то оно устанавливается равным нулю.
  • Если значение синей составляющей меньше, чем значение синего фильтра, то оно устанавливается равным нулю.
  • Любой пиксель, у которого эти значения слишком маленькие, будет черным.

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

Таблица 3. Индексы пикселей для изображения размером 4 х 4

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public void applyColorFilter(PApplet applet, IFAImage img, int minRed,
      int minGreen, int minBlue, int colorRange) {  
  img.loadPixels();
  int numberOfPixels = img.getPixels().length;
  for (int i = 0; i < numberOfPixels; i++) {
    int pixel = img.getPixel(i);
    float alpha = pixelColorHelper.alpha(applet, pixel);
    float red = pixelColorHelper.red(applet, pixel);
    float green = pixelColorHelper.green(applet, pixel);
    float blue = pixelColorHelper.blue(applet, pixel);

    red = (red >= minRed) ? red : 0;
    green = (green >= minGreen) ? green : 0;
    blue = (blue >= minBlue) ? blue : 0;

    image.setPixel(i, pixelColorHelper.color(applet, red, green, blue, alpha));
  }
}

Цвет

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

В предыдущем разделе мы использовали понятие "цветовое пространство", которое представляет собой способ представления цвета в цифровом виде. Дети, смешивая краски узнают, что цвета могут быть созданы из других цветов; в цифровом мире это происходит несколько по-другому (меньше риск испачкаться краской!), но похожим образом. Фреймворк Processing позволяет очень легко работать с любым цветовым пространством, которое вам нужно, но вы должны знать, какое из них выбрать, поэтому очень важно знать, как все это работает.

Цвета RGB

Цветовое пространство, с которым знакомо большинство программистов, - это пространство RGBA: красный, зеленый и синий цвета и альфа-канал; это то цветовое пространство, которое мы использовали выше. В шестнадцатеричной системе счисления (по основанию 16), первые две цифры обозначают количество красного цвета, вторые две цифры – синего цвета, третие две цифры – зеленого цвета, а последние две цифры (если они есть) являются альфа-значение. Диапазон значений от 00 по основанию 16 (0 – по иснованию 10) и до FF (до 255 по основанию 10). Альфа указывает на степень непрозрачности, где 0 указывает на полную прозрачность, а 100% - на полную непрозрачность.

Цвета HSB или HSV

Это цветовое пространство не так хорошо известно, как пространство RGB. Первое число представляет собой оттенок, второе число – степень насыщения (насколько интенсивен цвет), а третье число указывает яркость. Цветовое пространство HSV можно представить в виде конуса: Оттенок является положением по круговому сечению конуса, насыщение - расстояние от центра, а яркость - высота (значение 0 соответствует яркости черного цвета).

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

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

Когда я смотрю на мои серии фотографий, я вижу некоторую тему. Вечерняя серия фотографий, которую я сделала на закате с лодки в Гонконгской гавани, серая серия Северной Кореи, пышная зелень Бали, ледяной белый и бледно-голубой цвета исландской зимы. Можем ли мы взять фотографию и получить тот основной цвет, который доминирует в сцене?

Для этого имеет смысл использовать цветовое пространство HSB – когда мы хотим узнать, какой цвет основной, нам интересен оттенок. Это можно сделать и в пространстве RGB, но алгоритм будет гораздо сложнее (надо будет сравнивать все три значения) и он будет более чувствительным к яркостной составляющей. Мы можем перейти в цветовое пространство HSV с помощью команды colorMode.

Использовать это цветовое пространство проще, чем использовать пространство RGB. Нам нужно найти оттенок каждого пикселя, и выяснить, какой из них является самым "популярным". Очевидно, что нам не нужна точность – нам нужно сгруппировать очень похожие оттенки вместе, и мы можем справиться с этим с помощью следующих двух стратегий.

Во-первых, мы будем округлять десятичные дробные значения, которые станут целыми числами, поскольку в этом случае становится легко определить, в какую "корзину" мы помещаем каждый пиксель. Во-вторых, мы можем изменять допустимые отклонения значений оттенков. Если мы снова вернемся к представлению цветового пространства в виде конуса так, как это было сделано выше, то мы можем считать что это может быть любое значение из диапазона в 360 градусов (наподобие круга). В Processing по умолчанию используется максимальное значение 255, т.е. точно также, как это используется в пространстве RGB (255 - это FF в шестнадцатеричной системе). Чем допустимые отклонения значений будут шире, тем в изображении будут более отчетливые оттенки. Если использовать допустимые отклонения значений, то похожие оттенков можно будет группировать. При использовании диапазона в 360 градусов маловероятно, что мы сможем описать различие между оттенками со значениями 224 и 225, т.к. это различие очень мало. Если мы сократим диапазон до одной третьей, т.е. до 120, то после округления эти оба значения оттенка станут равными 75.

Мы можем изменить диапазон оттенков с помощью команды colorMode. Как только мы выполним команду colorMode(HSB, 120), то сразу сделаем диапазон чуть меньше половины в сравнении с тем, если бы мы использовали диапазон в 255 значений. Мы также знаем, что наши оттенки будут падать в одну из 120 "корзин", так что мы можем просто пробежаться по нашему изображению, получить значение оттенка пикселя, и добавить единицу к соответствующему счетчику в массиве. Сложность обхода O(n), где n - количество пикселей, поскольку действие нужно выполнить для каждого пикселя.

for(int px in pixels) {
  int hue = Math.round(hue(px));
  hues[hue]++;
}

Затем мы можем выдать этот оттенок на экран или показать на следующим рисунке (рис.6).

Рис.6. Зависимость доминантного оттенка от размера используемого диапазона (количества корзин)

После того, как мы извлекли "доминантный" оттенок, мы можем его в изображении либо показывать, либо скрывать. Мы можем показывать доминирующий оттенок в некотором приемлемом диапазоне значений (диапазоне, который мы выберем). Пиксели, которые не попадают в этот диапазон могут быть преобразованы в оттенки серого, причем значение будет выбрано с учетом значения яркости. На рис.7 показан доминирующий оттенок, который был определен в цветовом пространстве с диапазоном значений 240, и с разными допустимыми отклонениями.

Рис.7. Доминирующий оттенок отображается

Либо мы можем скрыть доминирующий оттенок. На рис.8, показаны рядом друг с другом несколько изображений: в центре показано оригинальное изображение, слева – изображение. В котором доминирующий цветовой тон отображается (сдвиг в сторону коричнего оттенка), а справа доминирующий оттенок скрыт (диапазон цветового пространства - 320, допустимое отклонение при отображении - 20 ).

Рис.8. Доминирующий оттенок скрыт

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

public HSBColor getDominantHue(PApplet applet, IFAImage image, int hueRange) {
  image.loadPixels();
  int numberOfPixels = image.getPixels().length;
  int[] hues = new int[hueRange];
  float[] saturations = new float[hueRange];
  float[] brightnesses = new float[hueRange];

  for (int i = 0; i < numberOfPixels; i++) {
    int pixel = image.getPixel(i);
    int hue = Math.round(pixelColorHelper.hue(applet, pixel));
    float saturation = pixelColorHelper.saturation(applet, pixel);
    float brightness = pixelColorHelper.brightness(applet, pixel);
    hues[hue]++;
    saturations[hue] += saturation;
    brightnesses[hue] += brightness;
  }

  // Поиск наиболее часто встречающегося оттенка
  int hueCount = hues[0];
  int hue = 0;
  for (int i = 1; i < hues.length; i++) {
    if (hues[i] > hueCount) {
      hueCount = hues[i];
      hue = i;
    }
  }

  // Возвращаем значение цвета
  float s = saturations[hue] / hueCount;
  float b = brightnesses[hue] / hueCount;
  return new HSBColor(hue, s, b);
}


public void processImageForHue(PApplet applet, IFAImage image, int hueRange,
    int hueTolerance, boolean showHue) {
  applet.colorMode(PApplet.HSB, (hueRange - 1));
  image.loadPixels();
  int numberOfPixels = image.getPixels().length;
  HSBColor dominantHue = getDominantHue(applet, image, hueRange);
  // Обрабатываем изображение, делаем серым любой пиксел, который не совпадает с этим оттенком
  float lower = dominantHue.h - hueTolerance;
  float upper = dominantHue.h + hueTolerance;
  for (int i = 0; i < numberOfPixels; i++) {
    int pixel = image.getPixel(i);
    float hue = pixelColorHelper.hue(applet, pixel);
    if (hueInRange(hue, hueRange, lower, upper) == showHue) {
      float brightness = pixelColorHelper.brightness(applet, pixel);
      image.setPixel(i, pixelColorHelper.color(applet, brightness));
    }
  }
  image.updatePixels();
}

Комбинация фильтров

С помощью интерфейса в том виде, как он есть, пользователь может комбинировать вместе красный, зеленый и синий фильтры. Если комбинировать фильтры с доминирующими оттенками с красным, зеленым и синим фильтрами, то из-за смены цветовых пространств результаты иногда могут быть немного неожиданными.

Во фреймворке Processing есть некоторые встроенные методы, которые поддерживают обработку изображений; например, invert и blur.

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

Перейти к следующей части статьи.