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

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

Поль Рейнерc расскажет о том, как создавать изображения с необычной и креативной анимацией при помощи Java™ 2D API и модели клеточного автомата (cellurar automata) (KA). По ходу статьи будет показана реализация операторов для работы с изображениями (image operator) в Java-коде и пояснена концепция циклического пространства (cyclic space), являющегося подвидом 2D-клеточного автомата. Идеи, изложенные в этой статье, можно использовать для создания собственных операторов для работы с изображениями и программ для работы с графикой с использованием Java-технологии.

Пол Рейнерс, программист, IBM

Пол Рейнерс (Paul Reiners) – программист и разработчик Java, сертифицированный Sun. Он автор нескольких open source программ, таких как Automatous Monk, Twisted Life и Leipzig. Пол получил степень магистра прикладной математики со специализацией в теории вычислений в университете Урбаны в Иллинойсе в мае 1991 г., т.е. примерно за 9 месяцев до того, как в том же университете впервые запустили HAL 9000 (это произошло 12 января 1992 г.). Он живет в Миннесоте и посвящает свое свободное время игре на электрогитаре, выступая в составе джаз-группы



22.01.2009

В этой статье рассказывается, как написать собственный 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);

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

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

Циклическое пространство было открыто Дэвидом Гриффитом (David Griffeath) с факультета математики Университета Висконсина в Мэдисоне (University of Wisconsin at Madison) и популяризовано A. K. Дьюдни (A. K. Dewdney) в журнале Scientific American (см. раздел Ресурсы).

В циклическом пространстве у каждой клетки есть одно из 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.

ConvolveOp почти является клеточным автоматом

Класс Java 2D API ConvolveOp представляет пространственную свертку (spatial convolution): Цвет каждого пикселя определяется при помощи комбинирования цветов исходного пикселя и его соседей.

Звучит знакомо? Этот класс почти является двухмерным клеточным автоматом. Например, состояния (цвета) скорее непрерывны, чем дискретны. (Это не совсем так: количество RGB-значений ограничено, но его достаточно для того, чтобы условно считать данную величину аналоговой.) Поэтому скорее упомянутый класс реализует непрерывный автомат (continuous automata). Кроме того, в непрерывном автомате, в отличие от клеточного автомата, отсутствует детальный контроль над текущем состоянием клетки и ее соседей.

По этим причинам через ConvolveOp нельзя определить циклическое пространство, но поднятая проблема все еще остается интересной. Это шанс взглянуть на ConvolveOp с другой точки зрения.

Это простое правило приводит к сложному и непредсказуемому поведению. В листинге 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. Жорж Сёра, 'Воскресный вечер на острове Гранд-Жатте'

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

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

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

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

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

Концептуальное искусство против алгоритмического искусства

Пока я готовился к написанию Seurat (первоначальное название этой программы - "Blue Poles"), то прочитал много трудов о Джексоне Поллоке (Jackson Pollock). Я ознакомился с основными понятиями концептуального искусства (process art). Я по наивности предполагал, что концептуальное искусство соответствует алгоритмическому искусству, в котором автор создает некий набор правил и выполняет его на совокупности входных значений, создавая тем самым художественный артефакт, например картину. Однако я обнаружил, что в мире искусства у process art есть другое определение. Согласно словарю коллекции Гугенхейма (Guggenheim Collection Glossary):

"В концептуальном искусстве (process art) акцент делается на сам процесс создания произведения (а не следование предопределенному плану) и на концепции изменения и быстротечности, что можно увидеть в работах Линды Бенглис (Lynda Benglis), Евы Хессе (Eva Hesse), Роберта Морриса (Robert Morris), Брюса Номана (Bruce Nauman), Алана Сарета (Alan Saret), Ричарда Сера (Richard Serra), Роберта Смитсона (Robert Smithson) и Кейт Сонер (Keith Sonnier)."

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

Как программисты, мы можем создавать концептуальные произведения или помогать художникам создавать их.

Рисунок 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), которая заинтересовала меня художественными искусствами.


Загрузка

ОписаниеИмяРазмер
исходный код Java для этой статьиj-j2D.zip19KB

Ресурсы

Научиться

  • 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-языка и клеточных автоматов для создания алгоритмических музыкальных произведений.
  • Introduction to Java 2D (EN) (Мич Гольдштейн, developerWorks, Июль 2002): пошаговое руководство по преимуществам продвинутого рисования, управления текстом и оперированием изображениями, которые Java 2D привнесла в GUI-программирование.
  • 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)
  • Safarit technology bookstore: Интернет-магазин технической литературы.(EN)
  • Раздел Технология Java : сайта developerWorks: сотни статей обо всех аспектах Java-программирования.

Получить продукты и технологии

  • IBM trial software: ознакомительные версии программного обеспечения для разработчика, которые можно загрузить со страницы developerWorks.(EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


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