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

UnixForum





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

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

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

Архитектура

Приложение состоит из трех частей (рис.9).

Приложение

Приложение состоит из одного файла: ImageFilterApp.java. Этот файл расширяет PApplet (суперкласс приложения Processing) и обрабатывает компоновку интерфейса layout, взаимодействие с пользователем и т.д. Этот класс является самым трудным для тестирования, поэтому мы хотим, чтобы он мог быть настолько маленьким, насколько это возможно.

Модель

Модель состоит из трех файлов: файла HSBColor.java, который является простым контейнером для цветов HSB (состоящих из оттенка, насыщенности и яркости), файла IFAImage, который является оберткой вокруг PImage и предназначен для возмжности выполнения тестирования. (В PImage есть ряд методов final, к которым не могут быть применены моки mock). И, наконец, файла ImageState.java, который является объектом, в котором описывается состояние изображения – фильтры какого уровня должны быть применены и конктретно какие фильтры, а также обработчики, с помощью которых изображение загружается. (Примечание: Всякий раз, когда изменяются настройки цветовых фильтров убираются, и всякий раз, когда доминирующий цвет пересчитывается, изображение должно быть перезагружено. Для ясности, мы просто перезагружаем изображение каждый раз, когда над ним осуществляется обработка).

Цвет

Работа с цветом происходит с использованием двух файлов: файла ColorHelper.java, в котором происходит вся обработка и фильтрация изображения, и файл PixelColorHelper.java, в котором для возможности тестирования цветов пикселей происходит абстрагирование методов PApplet, имеющих тип final.

Рис.9. Архитектура

Обертки вокруг классов и тестов

Как уже кратко упоминалось выше, есть два класса оберток (IFAImage и PixelColorHelper), которые изолируют библиотеку с целью обеспечения возможности тестирования. Это вызвано тем, что в языке Java, ключевое слово "final" указывает метод, который нельзя переопределить или скрыть с помощью подклассов, что означает, что к ним нельзя применять моки mock.

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

package com.catehuston.imagefilter.color;

import processing.core.PApplet;

public class PixelColorHelper {

  public float alpha(PApplet applet, int pixel) {
    return applet.alpha(pixel);
  }

  public float blue(PApplet applet, int pixel) {
    return applet.blue(pixel);
  }

  public float brightness(PApplet applet, int pixel) {
    return applet.brightness(pixel);
  }

  public int color(PApplet applet, float greyscale) {
    return applet.color(greyscale);
  }

  public int color(PApplet applet, float red, float green, float blue,
           float alpha) {
    return applet.color(red, green, blue, alpha);
  }

  public float green(PApplet applet, int pixel) {
    return applet.green(pixel);
  }

  public float hue(PApplet applet, int pixel) {
    return applet.hue(pixel);
  }

  public float red(PApplet applet, int pixel) {
    return applet.red(pixel);
  }

  public float saturation(PApplet applet, int pixel) {
    return applet.saturation(pixel);
  }
}

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

package com.catehuston.imagefilter.model;

import processing.core.PApplet;
import processing.core.PImage;

public class IFAImage {

  private PImage image;

  public IFAImage() {
    image = null;
  }

  public PImage image() {
    return image;
  }

  public void update(PApplet applet, String filepath) {
    image = null;
    image = applet.loadImage(filepath);
  }

  // Обертки методов из PImage.
  public int getHeight() {
    return image.height;
  }

  public int getPixel(int px) {
    return image.pixels[px];
  }

  public int[] getPixels() {
    return image.pixels;
  }

  public int getWidth() {
    return image.width;
  }

  public void loadPixels() {
    image.loadPixels();
  }

  public void resize(int width, int height) {
    image.resize(width, height);
  }

  public void save(String filepath) {
    image.save(filepath);
  }

  public void setPixel(int px, int color) {
    image.pixels[px] = color;
  }

  public void updatePixels() {
    image.updatePixels();
  }
}

И, наконец, у нас есть простой класс-контейнер HSBColor. Обратите внимание, что он немутируемый (однажды создается и никогда не меняется). Немутируемые объекты более предпочтительны для обеспечения потокобезопасности (иногда нам это не нужно!), и также более просты для понимания и их использования о. В общем, я, как правило, делаю простые классы-модели немутируемыми, если, конечно, я не найду вескую причину так не поступать, и в данном случае таких причин не возникало.

Некоторым из вас может быть известно, что во фреймворке Processing и в самом языке Java уже есть классы, представляющие цвет. Не вдаваясь здесь в детали обеиз случаев, оба они в большей степени предназначены для работы с цветами RGB, а класс Java, в частности, существенно более сложен, чем нам нужно. У нас, вероятно, не было бы проблем, если мы бы пользовались классом awt.Color языка Java; но компонентами графического интерфейса awt нельзя пользоваться во фреймворке Processing, поэтому для наших целей создается этот простой класс-контейнер для хранения всех этих битов данных, что намного проще.

package com.catehuston.imagefilter.model;

public class HSBColor {

  public final float h;
  public final float s;
  public final float b;

  public HSBColor(float h, float s, float b) {
    this.h = h;
    this.s = s;
    this.b = b;
  }
}

Класс ColorHelper и его тесты

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

package com.catehuston.imagefilter.color;

import processing.core.PApplet;

import com.catehuston.imagefilter.model.HSBColor;
import com.catehuston.imagefilter.model.IFAImage;

public class ColorHelper {

  private final PixelColorHelper pixelColorHelper;

  public ColorHelper(PixelColorHelper pixelColorHelper) {
    this.pixelColorHelper = pixelColorHelper;
  }

  public boolean hueInRange(float hue, int hueRange, float lower, float upper) {
    // Необходмо для округления значений
    if (lower < 0) {
      lower += hueRange;
    }
    if (upper > hueRange) {
      upper -= hueRange;
    }
    if (lower < upper) {
      return hue < upper && hue > lower;
    } else {
      return hue < upper || hue > lower;
    }
  }

  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();
  }

  public void applyColorFilter(PApplet applet, IFAImage image, int minRed,
      int minGreen, int minBlue, int colorRange) {
    applet.colorMode(PApplet.RGB, colorRange);
    image.loadPixels();
    int numberOfPixels = image.getPixels().length;
    for (int i = 0; i < numberOfPixels; i++) {
      int pixel = image.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));
    }
  }
}

Нам не требуется тестировать этот код на полноразмерных изображениях, поскольку нам будут нужны такие изображения, о свойсвах которых мы знаем все. Мы аппроксимируем их, заменяя на изображения-моки и делаем их такими, чтобы возвращался массив пикселей - в нашем случае их будет 5. В результате можно будет проверить, что поведение приложенния будет именно таким, как и ожидалось. Ранее мы рассмотрели принцип использования объектов-моков (фиктивных объектов), а здесь мы видим, как они используются. В качестве фреймворка для работы объектами-моками мы пользуемся фреймворком Mockito.

Чтобы создать мок-объект, мы применяем к экземпляру переменной аннотацию @Mock и она на этапе выполнения приложения будет обрабатываться раннером MockitoJUnitRunner как мок-объект.

Чтобы определить, как себя ведет метод, мы воспользуемся следующей конструкцией:

when(mock.methodCall()).thenReturn(value)

Чтобы проверить, что метод был вызван, мы используем конструкцию verify(mock.methodCall()).

Ниже мы покажем несколько примеров тест-кейсов; если вы хотите увидеть остальные тест-кейсы, помотрите папку с исходными кодами настоящего проекта, которая находится в репозитории 500 Lines or Less на GitHub.

package com.catehuston.imagefilter.color;

/* ... Импорты опущены ... */

@RunWith(MockitoJUnitRunner.class)
public class ColorHelperTest {

  @Mock PApplet applet;
  @Mock IFAImage image;
  @Mock PixelColorHelper pixelColorHelper;

  ColorHelper colorHelper;

  private static final int px1 = 1000;
  private static final int px2 = 1010;
  private static final int px3 = 1030;
  private static final int px4 = 1040;
  private static final int px5 = 1050;
  private static final int[] pixels = { px1, px2, px3, px4, px5 };

  @Before public void setUp() throws Exception {
    colorHelper = new ColorHelper(pixelColorHelper);
    when(image.getPixels()).thenReturn(pixels);
    setHsbValuesForPixel(0, px1, 30F, 5F, 10F);
    setHsbValuesForPixel(1, px2, 20F, 6F, 11F);
    setHsbValuesForPixel(2, px3, 30F, 7F, 12F);
    setHsbValuesForPixel(3, px4, 50F, 8F, 13F);
    setHsbValuesForPixel(4, px5, 30F, 9F, 14F);
  }

  private void setHsbValuesForPixel(int px, int color, float h, float s, float b) {
    when(image.getPixel(px)).thenReturn(color);
    when(pixelColorHelper.hue(applet, color)).thenReturn(h);
    when(pixelColorHelper.saturation(applet, color)).thenReturn(s);
    when(pixelColorHelper.brightness(applet, color)).thenReturn(b);
  }

  private void setRgbValuesForPixel(int px, int color, float r, float g, float b, 
            float alpha) {
    when(image.getPixel(px)).thenReturn(color);
    when(pixelColorHelper.red(applet, color)).thenReturn(r);
    when(pixelColorHelper.green(applet, color)).thenReturn(g);
    when(pixelColorHelper.blue(applet, color)).thenReturn(b);
    when(pixelColorHelper.alpha(applet, color)).thenReturn(alpha);
  }

    @Test public void testHsbColorFromImage() {
    HSBColor color = colorHelper.getDominantHue(applet, image, 100);
    verify(image).loadPixels();

    assertEquals(30F, color.h, 0);
    assertEquals(7F, color.s, 0);
    assertEquals(12F, color.b, 0);
  }

  @Test public void testProcessImageNoHue() {
    when(pixelColorHelper.color(applet, 11F)).thenReturn(11);
    when(pixelColorHelper.color(applet, 13F)).thenReturn(13);
    colorHelper.processImageForHue(applet, image, 60, 2, false);
    verify(applet).colorMode(PApplet.HSB, 59);
    verify(image, times(2)).loadPixels();
    verify(image).setPixel(1, 11);
    verify(image).setPixel(3, 13);
  }

  @Test public void testApplyColorFilter() {
    setRgbValuesForPixel(0, px1, 10F, 12F, 14F, 60F);
    setRgbValuesForPixel(1, px2, 20F, 22F, 24F, 70F);
    setRgbValuesForPixel(2, px3, 30F, 32F, 34F, 80F);
    setRgbValuesForPixel(3, px4, 40F, 42F, 44F, 90F);
    setRgbValuesForPixel(4, px5, 50F, 52F, 54F, 100F);

    when(pixelColorHelper.color(applet, 0F, 0F, 0F, 60F)).thenReturn(5);
    when(pixelColorHelper.color(applet, 20F, 0F, 0F, 70F)).thenReturn(15);
    when(pixelColorHelper.color(applet, 30F, 32F, 0F, 80F)).thenReturn(25);
    when(pixelColorHelper.color(applet, 40F, 42F, 44F, 90F)).thenReturn(35);
    when(pixelColorHelper.color(applet, 50F, 52F, 54F, 100F)).thenReturn(45);

    colorHelper.applyColorFilter(applet, image, 15, 25, 35, 100);
    verify(applet).colorMode(PApplet.RGB, 100);
    verify(image).loadPixels();

    verify(image).setPixel(0, 5);
    verify(image).setPixel(1, 15);
    verify(image).setPixel(2, 25);
    verify(image).setPixel(3, 35);
    verify(image).setPixel(4, 45);
  }
}

Обратите внимание, что:

  • Мы используем MockitoJUnit.
  • Мы используем моки PApplet, IFAImage (созданные исключительно для этого) и ImageColorHelper.
  • Тестовые методы аннотируются с помощью @Test[2]. Если вы хотите проигнорировать тест (например, в режиме отладки), то можете добавить аннотацию @Ignore.
  • В методе setup() мы создаем массив пикселей и изображение-мок должно всегда его возвращать.
  • Вспомогательные методы помогают задавать ожидаемые действия для повторяющихся задач (например, для set*ForPixel())

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