Библиотека сайта 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. Обратите внимание, что он немутируемый (однажды создается и никогда не меняется). Немутируемые объекты более предпочтительны для обеспечения потокобезопасности (иногда нам это не нужно!), и также более просты для понимания и их использования о. В общем, я, как правило, делаю простые классы-модели немутируемыми, если, конечно, я не найду вескую причину так не поступать, и в данном случае таких причин не возникало.
Некоторым из вас может быть известно, что во фреймворке и в уже есть классы, представляющие цвет. Не вдаваясь здесь в детали обеиз случаев, оба они в большей степени предназначены для работы с цветами RGB, а класс Java, в частности, существенно более сложен, чем нам нужно. У нас, вероятно, не было бы проблем, если мы бы пользовались классом awt.Color языка Java; но , поэтому для наших целей создается этот простой класс-контейнер для хранения всех этих битов данных, что намного проще.
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. В результате можно будет проверить, что поведение приложенния будет именно таким, как и ожидалось. Ранее мы рассмотрели принцип использования объектов-моков (фиктивных объектов), а здесь мы видим, как они используются. В качестве фреймворка для работы объектами-моками мы пользуемся фреймворком .
Чтобы создать мок-объект, мы применяем к экземпляру переменной аннотацию @Mock и она на этапе выполнения приложения будет обрабатываться раннером MockitoJUnitRunner как мок-объект.
Чтобы определить, как себя ведет метод, мы воспользуемся следующей конструкцией:
when(mock.methodCall()).thenReturn(value)
Чтобы проверить, что метод был вызван, мы используем конструкцию verify(mock.methodCall()).
Ниже мы покажем несколько примеров тест-кейсов; если вы хотите увидеть остальные тест-кейсы, помотрите папку с исходными кодами настоящего проекта, которая находится в .
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())
Перейти к следующей части статьи.
