Содержание


Пуантилизм и зернистость

Использование Java 2D API для создания анимированной графики

Comments

В этой статье рассказывается, как написать собственный Java-класс для обработки 2D-изображений при помощи интерфейса BufferedImageOp. При создании приложения, обрабатывающего изображение, этот интерфейс использует двухмерный клеточный автомат (КА) - циклическое пространство. КА "воздействует" на изображение (например, JPEG-файл), с течением времени причудливым образом преобразуя изображение. Данная статья покажет читателю возможности для написания приложений следующего поколения для обработки изображений.

2D клеточный автомат

2D-клеточный автомат состоит из клеток, являющихся узлами в двухмерной решетке, обычно называемой вселенной (universe). Каждая клетка имеет состояние, которое можно представить целочисленным значением от 0 До n. В листинге 1 показано, как в Java-коде объявить вселенную клеточного автомата (cellular automaton universe):

Листинг 1. Объявление TwoDCellularAutomaton.universe
protected int[][] universe;

На каждый такт воображаемых часов клетки одновременно обновляют свои состояния. Новое состояние клетки зависит по определенному правилу от текущего состояния клетки и текущих состояний соседних клеток. В листинге 2 происходит обновление состояния всех клеток вселенной за каждый такт:

Листинг 2. Класс TwoDCellularAutomaton (фрагмент кода)
public void update() {
    int[][] newUniverse = new int[rowCount][colCount];
    for (int row = 0; row < rowCount; row++) {
        for (int col = 0; col < colCount; col++) {
            newUniverse[row][col] = updateCell(row, col);
        }
    }
    for (int row = 0; row < rowCount; row++) {
        for (int col = 0; col < colCount; col++) {
            universe[row][col] = newUniverse[row][col];
        }
    }
}

protected abstract int updateCell(int row, int col);

Правило, которое определяет механизм обновления состояния ячеек, для каждого вида клеточного автомата свое собственное. Определение этого правила будет выполнено в подклассах.

Циклическое пространство

В циклическом пространстве у каждой клетки есть одно из n состояний. Начальное состояние каждой клетки выбирается случайным образом - произвольное число из диапазона от 0 до n - 1 (включительно). Соседние клетки называются фон-неймовановскими соседями (von Neumann neighborhood): четыре клетки сверху, снизу, справа и слева от рассматриваемой клетки.

В листинге 3 путем задания разницы координат рассматриваемой клетки и ее соседей были определены фон-неймановские соседи:

Листинг 3. Определение TwoDCellularAutomaton.VON_NEUMANN_NEIGHBORHOOD
protected static final int[][] VON_NEUMANN_NEIGHBORHOOD = { { -1, 0 },
        { 1, 0 }, { 0, -1 }, { 0, 1 } };

Циклическое пространство определяется по следующему правилу:

Если у клетки с состоянием k есть сосед с состоянием k + 1,тогда на следующем такте часов у рассматриваемой клетки будет состояние k + 1. В противном случае, состояние клетки останется тем же.

Это правило является циклическим. Таким образом, если состояние клетки n - 1, а у соседней клетки состояние 0, то у рассматриваемой клетки на следующем такте будет состояние 0.

Это простое правило приводит к сложному и непредсказуемому поведению. В листинге 4 реализовано правило для обновления клетки в циклическом пространстве:

Листинг 4. Определение CyclicSpace.updateCell(int, int)
protected int updateCell(int row, int col) {
    int[] neighborStates = getNeighborStates(row, col, neighborhood);
    int currentState = universe[row][col];
    for (int i = 0; i < neighborStates.length; i++) {
        int neighborState = neighborStates[i];
        if (neighborState == (currentState + 1) % n) {
            return neighborState;
        }
    }

    return currentState;
}

Как уже говорилось, начальное состояние циклической вселенной является случайным. Клетки "поедаются" клетками с "бОльшим" состоянием, и их состояние периодически возвращается к 0. Как только это случается, группы клеток самоорганизуются и распространяются, приобретая волнообразную природу. В конечном итоге образуется четкие волны. Эти волны двигаются по диагонали через вселенную и выглядят как шестеренки (pinwheels).

Создание фильтра-оператора для работы с изображениями

Интерфейс java.awt.image.BufferedImageOp позволяет создавать свой собственный оператор для работы с изображениями, называемый также фильтром. В данной статье рассматривается только один метод из интерфейса BufferedImageOp:

BufferedImage filter(BufferedImage src, BufferedImage dest)

Параметры src и dest являются 2-х мерными сетками пикселей. При реализации метода можно создать dest из src любым желаемым способом. Обычным подходом является проход через все пиксели в src и создание согласно какому-то правилу соответствующих пикселей в dest. Этот способ и был реализован в примере приложения для обработки изображений, которое было названо Seurat в честь знаменитого французского художника Жорж Пьера Сёра (Georges-Pierre Seurat) (чтобы получить полный исходный код этого приложения, см. раздел Материалы для скачивания).

Программа Seurat

Налицо сходство между пикселями в изображениями и клетками в КА. Оба они состоят из двухмерных сеток и каждый узел в них имеет состояние, которое в случае пикселя представляется RGB (Red, Green, Blue) значениями. Это природное сходство можно использовать в реализации фильтра filter(BufferedImage src, BufferedImage dest). Для каждого пикселя в src RGB-значения этого пикселя по определенному правилу связываются с состоянием соответствующей клетки в клеточном автомате, благодаря чему создаются новые RGB-значения в соответствующем пикселе в dest. Это правило и определяет фильтр.

В листинге 5 показано, как обойти все пиксели в src и создать пиксели в dest. Абстрактный метод getNewRGB(Color) определяется в самих реализациях фильтров. Он вычисляет и возвращает обработанные согласно фильтру RGB-значения для входного цвета.

Листинг 5. Класс CellularAutomataFilter (фрагмент кода)
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
    if (dest == null)
        dest = createCompatibleDestImage(src, null);

    int srcHeight = src.getHeight();
    int srcWidth = src.getWidth();
    for (int y = 0; y < srcHeight; y++) {
        for (int x = 0; x < srcWidth; x++) {
            //извлечь пиксель из исходного изображения.
            int origRGB = src.getRGB(x, y);
            Color origColor = new Color(origRGB);

            //извлечь из фильтра новые RGB значения.
            int[] newRGB = getNewRGB(origColor);

            //с помощью масштабирования преобразовать координаты пикселя в координаты КА.
			int cAY = (int) ((double) twoDCellularAutomaton
                    .getRowCount()
                    / (double) srcHeight * y);
            int cAX = (int) ((double) twoDCellularAutomaton
                    .getColCount()
                    / (double) srcWidth * x);
            //получить состояние соответствующей ячейки КА.
			int state = twoDCellularAutomaton.getState(cAY,
                    cAX);
            //определить вес фильтрованных RGB-значений в зависимости от состояния.
			double filterProportion = (double) state
                    / (double) twoDCellularAutomaton.getN();

            // определить взвешенное среднее значение для фильтрованных RGB-значений
            // и RGB-значений изображения.
            int weightedRed = (int) Math.round(newRGB[0] * filterProportion
                    + origColor.getRed() * (1.0 - filterProportion));
            int weightedBlue = (int) Math.round(newRGB[1]
                    * filterProportion + origColor.getBlue()
                    * (1.0 - filterProportion));
            int weightedGreen = (int) Math.round(newRGB[2]
                    * filterProportion + origColor.getGreen()
                    * (1.0 - filterProportion));

            // установить для пикселя в dest взвешенное усредненное значение.
			dest.setRGB(x, y, new Color(weightedRed, weightedBlue,
                    weightedGreen).getRGB());
        }
    }

    return dest;
}

abstract protected int[] getNewRGB(Color color);

Можно заметить, что отсутствует однозначное соответствие между пикселями в изображении и клетками в КА. КА является более "крупнозернистым" (по крайней мере, в большинстве случаев). Делается это из-за соображений производительности (чем больше зернистость КА - тем сильнее вычислительная нагрузка). Однако, используя различные размеры КА-вселенных, можно получить интересные эффекты зернистости (pixelation effects).

В листинге 6 показана одна из возможных реализаций getNewRGB(Color). Эта функция вычисляет значение, называемое "RGB-дополнение" ("RGB-complement"), которое не соответствует привычному определению дополнительного цвета (complement color). (Фильтр, вычисляющий истинные дополнительные цвета, было бы интересно запрограммировать, однако он весьма сложен для кодирования.)

Листинг 6. Класс RGBComplementFilter (фрагмент кода)
protected int[] getNewRGB(Color c) {
    int red = c.getRed();
    int newRed = getComplement(red);
    int green = c.getGreen();
    int newGreen = getComplement(green);
    int blue = c.getBlue();
    int newBlue = getComplement(blue);

    return new int[] { newRed, newGreen, newBlue };
}

private int getComplement(int colorVal) {
    // 'отразить' значение colorVal относительно средней величины - 128.
	int maxDiff = colorVal >= 128 ? -colorVal : 255 - colorVal;
    // разделить на 2.0 чтобы сделать эффект более тонким.
    // можно также использовать только значнеие maxDiff для большего выделения.
    int diff = (int) Math.round(maxDiff / 2.0);
    int newColorVal = colorVal + diff;

    return newColorVal;
}

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

И наконец, необходимо анимировать изображение, обновляя его по такту CA-часов. Для этого используется javax.swing.Timer. (Это простейший, но не самый лучший путь для анимирования изображения. В книге Джонатана Кнудсена (Jonathan Knudsen) Java 2D Graphics представлен лучший, более сложный способ создания анимации; см. раздел Ресурсы.)

Работа Seurat

На рисунке 1 представлена фотокопия шедевра Жоржа Сёра за 1884 год "Воскресный вечер на острове Гранд-Жатте" ("A Sunday Afternoon on the Island of La Grand Jatte"):

Рисунок 1. Жорж Сёра, "Воскресный вечер на острове Гранд-Жатте"
Рисунок 1. Жорж Сёра, 'Воскресный вечер на острове Гранд-Жатте'
Рисунок 1. Жорж Сёра, 'Воскресный вечер на острове Гранд-Жатте'

Теперь обработаем картину Сёра в программе Seurat при помощи фильтра создания RGB-дополнения. На рисунке 2 показана картина с наложенным фильтром в начальный момент циклического пространства (случайное состояние):

Рисунок 2. Картина, обработанная фильтром, в начальном случайном состоянии циклического пространства
Рисунок 2. Картина, обработанная фильтром, в начальном случайном состоянии циклического пространства
Рисунок 2. Картина, обработанная фильтром, в начальном случайном состоянии циклического пространства

На рисунке 3 показана обработанная фильтром картина, в которой циклическое пространство начинает самоупорядочиваться, но пока что в нем все еще много неорганизованности:

Рисунок 3. Картина, обработанная фильтром с циклическим пространством, в промежуточном состоянии
Рисунок 3. Картина, обработанная фильтром с циклическим пространством, в промежуточном состоянии
Рисунок 3. Картина, обработанная фильтром с циклическим пространством, в промежуточном состоянии

Рисунок 4 показывает заключительное состояние картины с наложенным фильтром:

Рисунок 4. Картина с наложенным фильтром в итоговом состоянии
Рисунок 4. Картина с наложенным фильтром в итоговом состоянии
Рисунок 4. Картина с наложенным фильтром в итоговом состоянии

На самом деле на рассматриваемой картине не были в полной мере реализованы возможности фильтра и КА. (В конце концов, это приложение было написано для создания анимированных изображений.) Поэтому читателю предлагается запустить Java-апплет чтобы увидеть анимацию, сделанную при помощи фильтра/КА (чтобы посмотреть online-demo, см. раздел Ресурсы).

Эстетические рассуждения

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

Программа Seurat запускалась на картинах различных жанров, получая интересные результаты для абстрактных и классических жанров. Однако, по всей видимости программа лучше всего работает с современным искусством - в частности поп-арт. Возникают очень интересные шаблоны на картине "Флаг" ("Flag") Джаспера Джонса (Jasper John). На этой картине диагональные линии циклического пространства хорошо "работают" в противовес прямым линиям. Seurat также показывает интересные результаты на абстрактных картинах Джексона Поллока (Jackson Pollock). Например, если циклическое пространство КА обработает картину "Синие столбы" ("Blue Poles"), оно скрывает, показывает и снова скрывает детали этой замечательной картины, концентрируя внимание в отдельные моменты времени на отдельных частях картины. Также хорошо обрабатываются работы фотографов. Я получил удовольствие, прогнав через программу сюрреалистические фотографии Ральфа Юджина Митярда (Ralph Eugene Meatyard).

При работе с приложением типа Seurat можно комбинировать три измерения: какой-либо подвид двухмерного клеточного автомата, фильтр и исходное изображение. В этой статье использовалось циклическое пространство, но можно использовать и другие типы 2-х мерных автоматов, такие как "перемешивающая машина" (hodgepodge). При программировании фильтра нас ограничивает только фантазия. В основном, эксперименты проводились с фильтрами, которые оперируют цветом, но также было бы интересно поработать с фильтрами, которые изменяют пространственные отношения в картине. Например, можно создать фильтр, который перекашивает изображение, наподобие эффекта, который использовался при создании обложки для альбома "Rubber Soul" ансамбля Beatles. В заключении, отмечу, что можно использовать любые изображения - хотя бы фотографии. Применительно к использованной в данной статье картины, различные комбинации фильтров и типов КА приведут к лучшим или худшим результатам. Я надеюсь, что эта статья проявит в читателе интерес к экспериментам в этой области.

Признание

Автор хотел бы поблагодарить Джулию Брасвелл (Julia Braswell), которая заинтересовала меня художественными искусствами.


Ресурсы для скачивания


Похожие темы

  • Pointillism meets pixelation: оригинал статьи (EN).
  • The Magic Machine: A Handbook of Computer Sorcery (A. K. Дьюдни, В. Х. Фриманн, 1990): Эта книга является сборником колонок Дьюдни "Computer Recreations" из журнала Scientific American; в книгу входит глава о циклическом пространстве и глава о "перемешивающей машине" (hodgepodge). Когда в 1980-м году колонка впервые появилась в журнале, я запрограммировал все приведенные в ней алгоритмы в AmigaBASIC на моей системе Amiga 500.(EN)
  • Primordial Soup Kitchen (EN): статья о клеточных автоматах от их первооткрывателя Дэвида Гриффита (David Griffeath), который открыл циклическое пространство.
  • Seurat: ознакомьтесь с Java-апплетом Seurat. (EN)
  • Java 2D Graphics (Джонатан Кнудсен, O'Reilly Media, 1999): эта книга является превосходным введением в тему графики в Java.(EN)
  • Java 2D API: документация, примеры и другие ресурсы по Java 2D. (EN)
  • Art: The Way It Is, 3rd ed. (Джон Адкинс Ричардсон, Prentice Hall and Harry N. Abrams, 1973): В этой книге рассказано о Сёра и других художниках. (EN)
  • Cellular automata and music (EN) (Поль Рейнерс, developerWorks, Май 2004): прочитайте об использовании Java-языка и клеточных автоматов для создания алгоритмических музыкальных произведений.
  • 2D animation with image-based paths (EN) (Барри Фейгенбаум и Том Брунет, developerWorks, январь 2004): в статье используются несжатые изображения, технология Swing и Java-процессор разработки автора для создания движущихся объектов в 2D-анимации.(EN)
  • Creating Java2D composites for rollover effects (EN) (Джо Винчестер и Рене Шварц, developerWorks, сентябрь 2002): статья о создании изображений и управлении ими при помощи Java 2D API.(EN)
  • Раздел Технология Java : сайта developerWorks: сотни статей обо всех аспектах Java-программирования.

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=463607
ArticleTitle=Пуантилизм и зернистость
publish-date=01222009