Рейтинг@Mail.ru

Наши друзья и партнеры

UnixForum
купить дешевый 
компьютер родом из Dhgate.com




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

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

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

Архитектура (продолжение)

Состояние изображения и его тесты

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

package com.catehuston.imagefilter.model;

import processing.core.PApplet;
import com.catehuston.imagefilter.color.ColorHelper;

public class ImageState {

  enum ColorMode {
    COLOR_FILTER,
    SHOW_DOMINANT_HUE,
    HIDE_DOMINANT_HUE
  }

  private final ColorHelper colorHelper;
  private IFAImage image;
  private String filepath;

  public static final int INITIAL_HUE_TOLERANCE = 5;

  ColorMode colorModeState = ColorMode.COLOR_FILTER;
  int blueFilter = 0;
  int greenFilter = 0;
  int hueTolerance = 0;
  int redFilter = 0;

  public ImageState(ColorHelper colorHelper) {
    this.colorHelper = colorHelper;
    image = new IFAImage();
    hueTolerance = INITIAL_HUE_TOLERANCE;
  }
  /* ... getters & setters */
  public void updateImage(PApplet applet, int hueRange, int rgbColorRange, 
          int imageMax) { ... }

  public void processKeyPress(char key, int inc, int rgbColorRange,
          int hueIncrement, int hueRange) { ... }

  public void setUpImage(PApplet applet, int imageMax) { ... }

  public void resetImage(PApplet applet, int imageMax) { ... }

  // For testing purposes only.
  protected void set(IFAImage image, ColorMode colorModeState,
            int redFilter, int greenFilter, int blueFilter, int hueTolerance) { ... }
}

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

package com.catehuston.imagefilter.model;

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

@RunWith(MockitoJUnitRunner.class)
public class ImageStateTest {

  @Mock PApplet applet;
  @Mock ColorHelper colorHelper;
  @Mock IFAImage image;

  private ImageState imageState;

  @Before public void setUp() throws Exception {
    imageState = new ImageState(colorHelper);
  }

  private void assertState(ColorMode colorMode, int redFilter,
      int greenFilter, int blueFilter, int hueTolerance) {
    assertEquals(colorMode, imageState.getColorMode());
    assertEquals(redFilter, imageState.redFilter());
    assertEquals(greenFilter, imageState.greenFilter());
    assertEquals(blueFilter, imageState.blueFilter());
    assertEquals(hueTolerance, imageState.hueTolerance());
  }

  @Test public void testUpdateImageDominantHueHidden() {
    imageState.setFilepath("filepath");
    imageState.set(image, ColorMode.HIDE_DOMINANT_HUE, 5, 10, 15, 10);

    imageState.updateImage(applet, 100, 100, 500);

    verify(image).update(applet, "filepath");
    verify(colorHelper).processImageForHue(applet, image, 100, 10, false);
    verify(colorHelper).applyColorFilter(applet, image, 5, 10, 15, 100);
    verify(image).updatePixels();
  }

  @Test public void testUpdateDominantHueShowing() {
    imageState.setFilepath("filepath");
    imageState.set(image, ColorMode.SHOW_DOMINANT_HUE, 5, 10, 15, 10);

    imageState.updateImage(applet, 100, 100, 500);

    verify(image).update(applet, "filepath");
    verify(colorHelper).processImageForHue(applet, image, 100, 10, true);
    verify(colorHelper).applyColorFilter(applet, image, 5, 10, 15, 100);
    verify(image).updatePixels();
  }

  @Test public void testUpdateRGBOnly() {
    imageState.setFilepath("filepath");
    imageState.set(image, ColorMode.COLOR_FILTER, 5, 10, 15, 10);

    imageState.updateImage(applet, 100, 100, 500);

    verify(image).update(applet, "filepath");
    verify(colorHelper, never()).processImageForHue(any(PApplet.class), 
                any(IFAImage.class), anyInt(), anyInt(), anyBoolean());
    verify(colorHelper).applyColorFilter(applet, image, 5, 10, 15, 100);
    verify(image).updatePixels();
  }

  @Test public void testKeyPress() {
    imageState.processKeyPress('r', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 5, 0, 0, 5);

    imageState.processKeyPress('e', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);

    imageState.processKeyPress('g', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 5, 0, 5);

    imageState.processKeyPress('f', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);

    imageState.processKeyPress('b', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 5, 5);

    imageState.processKeyPress('v', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);

    imageState.processKeyPress('h', 5, 100, 2, 200);
    assertState(ColorMode.HIDE_DOMINANT_HUE, 0, 0, 0, 5);

    imageState.processKeyPress('i', 5, 100, 2, 200);
    assertState(ColorMode.HIDE_DOMINANT_HUE, 0, 0, 0, 7);

    imageState.processKeyPress('u', 5, 100, 2, 200);
    assertState(ColorMode.HIDE_DOMINANT_HUE, 0, 0, 0, 5);

    imageState.processKeyPress('h', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);

    imageState.processKeyPress('s', 5, 100, 2, 200);
    assertState(ColorMode.SHOW_DOMINANT_HUE, 0, 0, 0, 5);

    imageState.processKeyPress('s', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);

    // Ключ со случайным значением никак не используется
    imageState.processKeyPress('z', 5, 100, 2, 200);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);
  }

  @Test public void testSave() {
    imageState.set(image, ColorMode.SHOW_DOMINANT_HUE, 5, 10, 15, 10);
    imageState.setFilepath("filepath");
    imageState.processKeyPress('w', 5, 100, 2, 200);

    verify(image).save("filepath-new.png");
  }

  @Test public void testSetupImageLandscape() {
    imageState.set(image, ColorMode.SHOW_DOMINANT_HUE, 5, 10, 15, 10);
    when(image.getWidth()).thenReturn(20);
    when(image.getHeight()).thenReturn(8);
    imageState.setUpImage(applet, 10);
    verify(image).update(applet, null);
    verify(image).resize(10, 4);
  }

  @Test public void testSetupImagePortrait() {
    imageState.set(image, ColorMode.SHOW_DOMINANT_HUE, 5, 10, 15, 10);
    when(image.getWidth()).thenReturn(8);
    when(image.getHeight()).thenReturn(20);
    imageState.setUpImage(applet, 10);
    verify(image).update(applet, null);
    verify(image).resize(4, 10);
  }

  @Test public void testResetImage() {
    imageState.set(image, ColorMode.SHOW_DOMINANT_HUE, 5, 10, 15, 10);
    imageState.resetImage(applet, 10);
    assertState(ColorMode.COLOR_FILTER, 0, 0, 0, 5);
  }
}

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

  • Мы делаем доступным для тестирования защищенный инициализационный метод set, что помогает нам быстро при тестировании определить конкретное состоянии системы.
  • Мы используем моки PApplet, ColorHelper и IFAImage (созданные специально для этой цели).
  • На этот раз мы пользуемся вспомогательным методом (assertState()) для того, чтобы было проще проверять утверждения о состоянии изображения.

Измерение тестового покрытия

Я пользуюсь в Eclipse плагином EclEmma для измерения тестового покрытия. Его можно установить из Eclipse marketplace.

В целом у нас для приложения тестовое покрытие составляет 81%, при этом ничего из ImageFilterApp тестами не покрывается, для ImageState покрытие составляет 94,8%, а для ColorHelper покрытие составляет 100%.

Приложение ImageFilterApp

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

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

package com.catehuston.imagefilter.app;

import java.io.File;

import processing.core.PApplet;

import com.catehuston.imagefilter.color.ColorHelper;
import com.catehuston.imagefilter.color.PixelColorHelper;
import com.catehuston.imagefilter.model.ImageState;

@SuppressWarnings("serial")
public class ImageFilterApp extends PApplet {

  static final String INSTRUCTIONS = "...";

  static final int FILTER_HEIGHT = 2;
  static final int FILTER_INCREMENT = 5;
  static final int HUE_INCREMENT = 2;
  static final int HUE_RANGE = 100;
  static final int IMAGE_MAX = 640;
  static final int RGB_COLOR_RANGE = 100;
  static final int SIDE_BAR_PADDING = 10;
  static final int SIDE_BAR_WIDTH = RGB_COLOR_RANGE + 2 * SIDE_BAR_PADDING + 50;

  private ImageState imageState;

  boolean redrawImage = true;

  @Override
  public void setup() {
    noLoop();
    imageState = new ImageState(new ColorHelper(new PixelColorHelper()));

    // Настройка выдачи изображения view.
    size(IMAGE_MAX + SIDE_BAR_WIDTH, IMAGE_MAX);
    background(0);

    chooseFile();
  }

  @Override
  public void draw() {
    // Рисуем изображение
    if (imageState.image().image() != null && redrawImage) {
      background(0);
      drawImage();
    }

    colorMode(RGB, RGB_COLOR_RANGE);
    fill(0);
    rect(IMAGE_MAX, 0, SIDE_BAR_WIDTH, IMAGE_MAX);
    stroke(RGB_COLOR_RANGE);
    line(IMAGE_MAX, 0, IMAGE_MAX, IMAGE_MAX);

    // Рисуем красную линию
    int x = IMAGE_MAX + SIDE_BAR_PADDING;
    int y = 2 * SIDE_BAR_PADDING;
    stroke(RGB_COLOR_RANGE, 0, 0);
    line(x, y, x + RGB_COLOR_RANGE, y);
    line(x + imageState.redFilter(), y - FILTER_HEIGHT,
        x + imageState.redFilter(), y + FILTER_HEIGHT);

    // Рисуем зеленую линию
    y += 2 * SIDE_BAR_PADDING;
    stroke(0, RGB_COLOR_RANGE, 0);
    line(x, y, x + RGB_COLOR_RANGE, y);
    line(x + imageState.greenFilter(), y - FILTER_HEIGHT,
        x + imageState.greenFilter(), y + FILTER_HEIGHT);

    // Рисуем синию линию
    y += 2 * SIDE_BAR_PADDING;
    stroke(0, 0, RGB_COLOR_RANGE);
    line(x, y, x + RGB_COLOR_RANGE, y);
    line(x + imageState.blueFilter(), y - FILTER_HEIGHT,
        x + imageState.blueFilter(), y + FILTER_HEIGHT);

    // Рисуем белую линию
    y += 2 * SIDE_BAR_PADDING;
    stroke(HUE_RANGE);
    line(x, y, x + 100, y);
    line(x + imageState.hueTolerance(), y - FILTER_HEIGHT,
        x + imageState.hueTolerance(), y + FILTER_HEIGHT);

    y += 4 * SIDE_BAR_PADDING;
    fill(RGB_COLOR_RANGE);
    text(INSTRUCTIONS, x, y);
    updatePixels();
  }

  // Обратный вызов для selectInput(), должен быть public для того, чтобы его можно было найти
  public void fileSelected(File file) {
    if (file == null) {
      println("User hit cancel.");
    } else {
      imageState.setFilepath(file.getAbsolutePath());
      imageState.setUpImage(this, IMAGE_MAX);
      redrawImage = true;
      redraw();
    }
  }

  private void drawImage() {
    imageMode(CENTER);
    imageState.updateImage(this, HUE_RANGE, RGB_COLOR_RANGE, IMAGE_MAX);
    image(imageState.image().image(), IMAGE_MAX/2, IMAGE_MAX/2, 
                imageState.image().getWidth(), imageState.image().getHeight());
    redrawImage = false;
  }

  @Override
  public void keyPressed() {
    switch(key) {
    case 'c':
      chooseFile();
      break;
    case 'p':
      redrawImage = true;
      break;
    case ' ':
      imageState.resetImage(this, IMAGE_MAX);
      redrawImage = true;
      break;
    }
    imageState.processKeyPress(key, FILTER_INCREMENT, RGB_COLOR_RANGE, 
                HUE_INCREMENT, HUE_RANGE);
    redraw();
  }

  private void chooseFile() {
    // Выбираем файл
    selectInput("Select a file to process:", "fileSelected");
  }
}

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

  • Наша реализация расширяет PApplet.
  • Большая часть работы выполняется в ImageState.
  • Метод fileSelected() является обратным вызовом метода selectInput().
  • Константы static final определены в начале кода.

Важность прототипирования

В реальном мире программирования мы тратим много времени на то, чтобы сделать из приложения отчуждаемое изделие. Дело обстоит следующим образом. 99,9% всего времени занимает поддержка приложения. Мы тратим много времени на поиски тех мест, где необходима более аккуратная работа алгоритмов.

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

В конце концов, я решила портировать свой результат в нативное мобильное приложение. Во фреймворке Processing есть библиотека для Android, но, как поступают многие разработчики мобильных приложений, я решила сначала перейти iOS. Несмотря на то, что мало имела дел с CoreGraphics, у меня имеется многолетний опыт работы с iOS; но я не думаю, что даже если бы у меня эта идея возникла с самого начала, я бы смогла сделать все на iOS. Платформа заставляет меня работать в цветовом пространстве RGB, и из-за этого становится труднее извлкать пиксели из изображения (привет, язык C). Основным ограничивающим фактиром является объем памяти и время ожидания результата.

Были и волнующий моменты, когда приложение проработало первый раз. Когда оно впервые проработало на моем устройстве ... без сбоев. Когда я сумела оптимизировать использование памяти на 66% и сократить на секунды время его выполнения. И были длительные промежутки, когда устройство лежало без дела.

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

Итак, как можно извлечь доминирующийо цвет из изображения? Для этого есть приложение: Show & Hide.

  1. Если мы хотим создать анимированный эскиз, нам не нужно пользоваться методом noLoop() (или, если мы хотим запустить анимацию позже, нам нужно вызвать метод loop()). Частота смена изображений при анимации задается с помощью метода frameRate().
  2. Имена методов в тестах не обязательно должны начинаться с префикса test так, как это принято в JUnit4, но от привычки изменять трудно.

Вернуться к началу статьи.

Если вам понравилась статья, поделитесь ею с друзьями: