Библиотека сайта 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
. Обратите внимание, что он немутируемый (однажды создается и никогда не меняется). Немутируемые объекты более предпочтительны для обеспечения потокобезопасности (иногда нам это не нужно!), и также более просты для понимания и их использования о. В общем, я, как правило, делаю простые классы-модели немутируемыми, если, конечно, я не найду вескую причину так не поступать, и в данном случае таких причин не возникало.
Некоторым из вас может быть известно, что во фреймворке 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()
)
Перейти к следующей части статьи.