logo
+7 (495) 997-37-74
Москва, ул.Международная, 15

Работа в Processing с изображениями и пикселями

В этой статье мы разберём один из фундаментальных аспектов системы Processing — работу с изображениями и отдельными пикселями. Хорошее понимание этих вопросов поможет вам уверенно себя чувствовать в процессе воплощения своих проектов в системе Processing.

Класс PImage

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

Вот базовый пример загрузки изображения из файла и отображения его в графическом окне:

PImage img;

void setup() {
  size(320, 240);
  img = loadImage("myimage.jpg");
}

void draw() {
  background(0);
  image(img, 0, 0);
}

Тут всё довольно прозрачно и очевидно: сначала объявляется переменная типа PImage с названием «img». Затем с помощью метода loadImage() создается новый экземпляр объекта PImage. В качестве аргумента указывается имя загружаемого в память файла. loadImage() загружает файлы изображений, хранящиеся в папке «data» скетча.

Processing поддерживает работу со следующими форматами файлов изображений: GIF, JPG, TGA, PNG.

Для создания пустого изображения используется функция createImage().

PImage img = createImage(200, 200, RGB);

Нужно также отметить, что процесс загрузки изображения из файла в память является медленным и мы должны убедиться, что наша программа делает это только один раз в setup(). Попытка загрузки изображений в теле функции draw() может привести к снижению производительности, а также к ошибкам типа «недостаточно памяти».

После загрузки изображение отображается с помощью функции image(). Эта функция содержит 3 аргумента — само изображение и координаты по X и Y. Дополнительно можно добавить ещё два аргумента для изменения размера изображения до определённых ширины и высоты, например:

image(img, 10, 20, 90, 60);

Фильтры изображений

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

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

PImage sunflower = loadImage("sunflower.jpg");
PImage dog = loadImage("dog.jpg");
background(dog);

Если tint() используется с одним аргументом, то изменяется только яркость изображения.

tint(255); // Изображение сохраняет исходное состояние
image(sunflower, 0, 0);

 

tint(100); // Изображение становится темнее
image(sunflower, 0, 0);

Второй аргумент изменит альфа-прозрачность картинки.

tint(255, 127); // Изображение становится прозрачным на 50%
image(sunflower, 0, 0);

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

tint(0, 200, 255);
image(sunflower,0,0);

Добавление четвёртого аргумента изменяет альфа канал (так же, как в случае с двумя аргументами). Диапазон значений tint() можно также задать с помощью функции colorMode().

tint(255, 0, 0, 100); // Изображение тонировано красным и прозрачно
image(sunflower, 0, 0);
 

Пиксели

Когда мы вызываем функции Processing, которые выполняют действия «нарисовать линию между двумя точками», «заполнить эллипс красным цветом» или «загрузить изображение JPG и поместить его на экран», внутри системы работает код, который переводит эти вызовы функций в установку отдельных пикселей на экране. Линия не появляется, потому что мы пишем line(), она появляется потому, что мы (код внутри Processing) окрашиваем все пиксели вдоль линейного пути между двумя точками.

Но часто нам требуется изменять отдельные пиксели напрямую. Processing обеспечивает эту функциональность через механизм «массива пикселей». Каждый пиксель на экране имеет положение X и Y. Однако пиксели массива имеют только одну размерность, сохраняя значения цвета в линейной последовательности.

Рассмотрим следующий пример: эта программа устанавливает для каждого пикселя случайное значение оттенков серого. Массив пикселей похож на другие массивы, единственное отличие состоит в том, что его не нужно объявлять, так как он является встроенной переменной Processing.

Пример работы с массивом пикселей:

size(200, 200);

loadPixels();

for (int i = 0; i < pixels.length; i++) {
  float rand = random(255);
  color c = color(rand);
  pixels[i] = c;
}

updatePixels(); 

Здесь функция loadPixels() вызывается до того, как вы получите доступ к массиву пикселей, а функция updatePixels() — после того, как вы закончите само изменение (редактирование) массива пикселей.

В приведенном примере, поскольку цвета заданы случайным образом, нам не пришлось беспокоиться о том, где пиксели находятся на экране, так как мы просто изменяем все пиксели без учета их расположения. Однако во многих приложениях местоположение X и Y самих пикселей является существенным.

Простым примером может быть установка для каждого четного столбца пикселей белого, а для каждого нечетного — черного цвета. Как это сделать с помощью одномерного пиксельного массива? Мы можем сделать это по следующей формуле:

1. Окно или изображение имеют WIDTH (ширину) и HEIGHT (высоту).

2. Массив пикселей имеет общее количество элементов, равное WIDTH * HEIGHT.

3. Для любой точки X и Y в окне соответствующее расположение в 1-мерном пиксельном массиве вычисляется по формуле: LOCATION = X + Y * WIDTH.

Вот код, реализующий раскраску столбцов при помощи рассмотренной выше формулы:

size(200, 200);

loadPixels();

for (int x = 0; x < width; x++) {
  for (int y = 0; y < height; y++) {
    int loc = x + y * width;
    if (x % 2 == 0) {
      pixels[loc] = color(255);
    } else {
      pixels[loc] = color(0);
    }
  }
}

updatePixels(); 

Обработка изображений

В предыдущем разделе рассматривались примеры, которые устанавливают значения пикселей в соответствии с произвольным вычислением. Теперь рассмотрим как можно установить пиксели в соответствии с существующим объектом PImage. Нам нужно будет для каждого пикселя в PImage извлечь его цвет и установить цвет соответствующего пикселя на дисплее.

Класс PImage включает данные, относящиеся к изображению — ширина, высота и пиксели. Доступ к этим данным (переменным) осуществляется через синтаксис точек.

PImage img = createImage(320, 240, RGB);

println(img.width);  // 320
println(img.height); // 240

img.pixels[0] = color(255,0,0); // Делает первый пиксель изображения красным

А теперь полный пример, реализующий поставленную задачу:

PImage img;

void setup() {
  size(200, 200);
  img = loadImage("sunflower.jpg");
}

void draw() {
  loadPixels(); 
  img.loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int loc = x + y*width;
      
      float r = red(img.pixels[loc]);
      float g = green(img.pixels[loc]);
      float b = blue(img.pixels[loc]);
      
      pixels[loc] =  color(r, g, b); // Установить пиксель дисплея в соответствии пикселю изображения
    }
  }
  updatePixels();
}

Он показывает базовый алгоритм для получения значений красного, зеленого и синего цветов для каждого пикселя на основе его расположения X и Y).

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

int imageLoc = x + y*img.width;
int displayLoc = x + y*width;

Фильтр изображений

Метод «обработка пиксель за пикселем» позволит создавать пользовательские алгоритмы для изменения цветов изображения. Например, изменение яркости — более яркие цвета имеют более высокие значения красных, зеленых и синих компонентов. Мы можем изменять яркость изображения, увеличивая или уменьшая цветовые компоненты каждого пикселя. В следующем примере мы динамически увеличиваем или уменьшаем эти значения в зависимости от положения мыши по горизонтали.

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

for (int x = 0; x < img.width; x++) {
  for (int y = 0; y < img.height; y++ ) {
    int loc = x + y*img.width;

    float r = red   (img.pixels[loc]);
    float g = green (img.pixels[loc]);
    float b = blue  (img.pixels[loc]);

    float adjustBrightness = ((float) mouseX / width) * 8.0;

    r *= adjustBrightness;
    g *= adjustBrightness;
    b *= adjustBrightness;

    r = constrain(r, 0, 255);
    g = constrain(g, 0, 255);
    b = constrain(b, 0, 255);

    color c = color(r, g, b);
    pixels[loc] = c;
  }
}

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

Пример изменения яркости пикселей в зависимости от расстояния пикселя от курсора мыши:

for (int x = 0; x < img.width; x++) {
  for (int y = 0; y < img.height; y++ ) {

    int loc = x + y*img.width;

    float r = red   (img.pixels[loc]);
    float g = green (img.pixels[loc]);
    float b = blue  (img.pixels[loc]);

    float distance = dist(x,y,mouseX,mouseY);
    float adjustBrightness = (50-distance)/50;

    r *= adjustBrightness;
    g *= adjustBrightness;
    b *= adjustBrightness;

    r = constrain(r,0,255);
    g = constrain(g,0,255);
    b = constrain(b,0,255);

    color c = color(r,g,b);
    pixels[loc] = c;
  }
}

 

Запись пикселей в объект PImage

Наши предыдущие примеры обработки изображений считывают каждый пиксель из исходного изображения и записывают новый пиксель непосредственно в окно обработки. Однако часто удобнее записывать новые пиксели в целевое изображение, которое затем отображается с помощью функции image(). Продемонстрируем эту технику при помощи операции threshold (порог).

Пороговый фильтр отображает каждый пиксель изображения только в одном из двух состояний — черном или белом. Это состояние устанавливается в соответствии с определенным пороговым значением: если яркость пикселя больше порога, мы окрашиваем пиксель в белый цвет, если меньше, то в черный. В приведенном ниже коде мы используем произвольное пороговое значение 100.

Пример применения порога яркости.

PImage source;
PImage destination;

void setup() {
  size(200, 200);
  source = loadImage("sunflower.jpg");  
  destination = createImage(source.width, source.height, RGB);
}

void draw() {  
  float threshold = 127;

  source.loadPixels();
  destination.loadPixels();
  
  for (int x = 0; x < source.width; x++) {
    for (int y = 0; y < source.height; y++ ) {
      int loc = x + y*source.width;

      if (brightness(source.pixels[loc]) > threshold) {
        destination.pixels[loc]  = color(255);
      }  else {
        destination.pixels[loc]  = color(0);
      }
    }
  }

  destination.updatePixels();
  image(destination, 0, 0);
}

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

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

image(img, 0, 0);
filter(THRESHOLD, 0.5);

Визуализация изображений

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

Ниже приведен ещё один пример алгоритма для рисования. Здесь мы выбираем цвета из пикселей внутри объекта PImage и в каждом цикле draw() рисуем один эллипс в случайном месте на экране. В результате получается «pointillism» эффект:

PImage img;
int pointillize = 16;

void setup() {
  size(200,200);
  img = loadImage("sunflower.jpg");
  background(0);
  smooth();
}

void draw() {
  int x = int(random(img.width));
  int y = int(random(img.height));
  int loc = x + y*img.width;
  
  loadPixels();
  float r = red(img.pixels[loc]);
  float g = green(img.pixels[loc]);
  float b = blue(img.pixels[loc]);
  noStroke();
  
  fill(r,g,b,100);
  ellipse(x,y,pointillize,pointillize);
}

Таким образом экспериментируя и применяя свою творческую фантазию, вы можете добиться очень интересных результатов в обработке изображений.

Заключение

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

Ссылки по теме

Система программирования Processing

Processing и Ардуино. Работа по Serial

Сетевые возможности Processing

Работа Processing в браузере и на сайте

Веб-сервер на Processing. Часть 1 — Код сервера

Веб-сервер на Processing. Часть 2 — Файлы и код на HTML и CSS

Работа в Processing со строками и вывод текста

Работа в Processing с изображениями и отдельными пикселями