Chapter 71

画像処理

miku
miku
2021.11.25に更新

画像の編集というのは、Photoshopのようなソフトウェアで実行するのが普通だが、最近ではブラウザ上でも編集できるサイトが増えてきている。この章ではピクセル一つ一つに対して処理を加える画像処理について扱う。たとえばCanvasのサイズが1920x1080の場合、200万以上のピクセルに対して処理を行う必要があるが、工夫次第では高速に処理ができるし、この本では扱わないが、GPUの機能だとまとめて実行できる。

ネガやモザイクなど画像処理の具体的なアルゴリズムについては以降の章から一つ一つ扱うので、この章では画像処理に必要な基礎知識や共通処理について解説を行う。

画像処理の基本

画像処理はピクセル一つ一つに対して一定のルールを適用する。ほとんどの処理は下記のようなコードになる。

let img;

function preload() {
  img = loadImage("./0.png");
}

function setup() {
  createCanvas(img.width, img.height);

  img.loadPixels();

  for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
      const pixel = getPixel(x, y);

      // pixel に対してなにかの処理を加える

      setPixel(x, y, [nr, ng, nb]);
    }
  }

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

function getPixel(x, y) {
  const i = (y * img.width + x) * 4;
  return [img.pixels[i], img.pixels[i + 1], img.pixels[i + 2], img.pixels[i + 3]];
}

function setPixel(x, y, pixel) {
  const i = (y * img.width + x) * 4;
  img.pixels[i + 0] = pixel[0];
  img.pixels[i + 1] = pixel[1];
  img.pixels[i + 2] = pixel[2];
}

画像処理の対象となる画像を読み込んだ後、その画像を pixels 配列に移すため loadPixels() を実行する。二重ループで一つ一つのピクセルを取り出す。ピクセルというのはRGBを一つの変数として扱いやすくするためにしたものなので、RGBごとに分解する必要がある。RGBそれぞれに処理を加えた後、またRGBを一つの変数に戻し、もとの座標のピクセルに設定する。対象となるピクセルを全て処理し終えた後、画面に表示するため updatePixels() で反映する。

次に作例として画像を明るくするアルゴリズムを紹介する。

明るくする

const k = 50;

const pixel = getPixel(x, y);

const r = red(pixel);
const g = green(pixel);
const b = blue(pixel);

const nr = min(r + k, 255);
const ng = min(g + k, 255);
const nb = min(b + k, 255);

setPixel(x, y, [nr, ng, nb]);

ピクセルを取り出し、RGBに分解した後、RGBそれぞれに定数 k を足し合わせる。k の値が大きいほど明るくなる。ただし、RGBの上限値は 255 なので、この数を超えないように調整する必要がある。全体のコードは下記のとおりになる。

const k = 50;
let img;

function preload() {
  img = loadImage("./0.png");
}

function setup() {
  createCanvas(img.width, img.height);
  pixelDensity(1);

  img.loadPixels();

  for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
      const pixel = getPixel(x, y);

      const r = red(pixel);
      const g = green(pixel);
      const b = blue(pixel);

      const nr = min(r + k, 255);
      const ng = min(g + k, 255);
      const nb = min(b + k, 255);

      setPixel(x, y, [nr, ng, nb]);
    }
  }

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

function getPixel(x, y) {
  const i = (y * img.width + x) * 4;
  return [img.pixels[i], img.pixels[i + 1], img.pixels[i + 2], img.pixels[i + 3]];
}

function setPixel(x, y, pixel) {
  const i = (y * img.width + x) * 4;
  img.pixels[i + 0] = pixel[0];
  img.pixels[i + 1] = pixel[1];
  img.pixels[i + 2] = pixel[2];
}

k の数を負数にすれば逆に画像が暗くなる。その場合は計算結果が 0 以上になるようにクランプする必要がある。たとえば k の数を自由にできるように constrain(r + k, 0, 255) としてもいい。

計算結果を別配列に格納する

これも先に話しておきたい。上記の明るくする処理のようなものは一つのピクセルを編集するだけなのでもとの配列を上書きすることができる。しかし、画像処理の内容によっては他のピクセルの値を参照する場合がある。

たとえば簡単のために左のピクセルを自身に入れるという画像処理をするとしよう。(x, y) にあるピクセルの中身は (x - 1, y) の値を入れることになる。では次に (x + 1, y) の処理をするとき、(x, y) の値を入れることになるが、これはすでに上書きされた(x - 1, y) の値が入っているはずだ。これでは延々と (x - 1, y) の値が伝搬することになる。

なので、このような場合はもとの配列とは違う新たな配列を用意して、そこに更新するピクセルを入れておく。最終的に画面に描画するための配列は後者のものを採用する。

const k = 50;
let img;

function preload() {
  img = loadImage("./0.png");
}

function setup() {
  createCanvas(img.width, img.height);

  const dest = createImage(img.width, img.height);
  dest.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
  dest.loadPixels();

  for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
      const pixel = getPixel(img, x, y);

      const r = red(pixel);
      const g = green(pixel);
      const b = blue(pixel);

      const nr = min(r + k, 255);
      const ng = min(g + k, 255);
      const nb = min(b + k, 255);

      setPixel(dest, x, y, [nr, ng, nb]);
    }
  }

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

function getPixel(img, x, y) {
  const i = (y * img.width + x) * 4;
  return [img.pixels[i], img.pixels[i + 1], img.pixels[i + 2], img.pixels[i + 3]];
}

function setPixel(img, x, y, pixel) {
  const i = (y * img.width + x) * 4;
  img.pixels[i + 0] = pixel[0];
  img.pixels[i + 1] = pixel[1];
  img.pixels[i + 2] = pixel[2];
}

getPixel(), setPixel() を拡張して、第一引数に pixels[] のもととなるImageオブジェクトを指定できるようにする。img とサイズが同じの新たなImageオブジェクト dest を作り、getPixel() には imgsetPixel() には dest を指定すると、画像処理の結果が dest に反映されて img には影響が出ない。