Chapter 06

正規化・線形補間・マップ

miku
miku
2021.11.25に更新

正規化

正規化という言葉には色々な意味があるが、ここでいう正規化は値を割合に直すことだ。つまりある値を 0~1 の範囲に変換する。

割合を知るには最大値が必要だろう。たとえば、お気に入りの野球選手が現在の時点で30本の安打を打っているとしよう。この選手の打率を知りたいのだが、そもそも何回バットを振ったのを知らないといけない。つまり打数という最大値が必要だ。

もし、打数が 80 だった場合は 30 / 80 = 0.375 で打率が3割7分5厘ということがわかる。これが正規化の作業だ。

安打が 0 のときは打率は 0、30本中30本打っているなら打率は 1 なので、

対象の値 / 上限値

で計算することができるだろう。

ただし、範囲の下限が 0 ではないケースもある。たとえば範囲が 20~100 の場合で、対象の値が 50 の場合、50 / 100 = 0.5 で、これは真ん中にある値だね、というのは明らかにおかしい。なので 対象の値 / 上限値 というのは下限が 0 でない場合は利用できないということだ。

この場合の解決方法として、下限が 0 でないなら、下限が 0 になるように範囲の幅をずらしてしまえばいい。

20~100 の範囲の場合、下限/上限とも下限の値である 20 を引いて 0~80 の範囲に変換する。対象の値が 50 の場合、同じように 20 を引いて 50 - 20 = 30 に変換する。それぞれの値自体は計算によって変わりはしたが、同じ値を引いているので、対象の値が範囲の中でのポジションが変わることはない。

あとは先程の手順通り、対象の値を上限値で割ることで正規化できる。

(対象の値 - 下限値) / (上限値 - 下限値)

計算式がわかったので実際にコードにしてみよう。

function norm(v, a, b) {
  return (v - a) / (b - a);
}

正規化の計算をする norm() は、a~b の範囲の中で v の位置にある割合を返す。p5.jsには同名同機能の関数が用意されているので今後はそちらを使用する。

const x1 = 200;
const x2 = 300;
const x = 250;

// 0.5
const ratio = norm(x, x1, x2);

下限値~上限値の間にある値を割合に直すイメージ図の作例。

線形補間

正規化の処理とは逆に、範囲と割合を渡すと、その割合の位置にある値を返す機能を線形補間と呼ぶ。

  • 正規化は 下限値, 上限値, 値 から 割合 を算出する
  • 線形補間は 下限値, 上限値, 割合 から を算出する

打数が80本、安打が30本の選手の打率は 30 / 80 = 0.375 だったが、
打数が80本、打率が0.375の選手の安打数は 80 * 0.375 = 30 となる。

前者が正規化で、後者が線形補間の計算だ。

正規化: 値 / 上限値 = 割合
線形補間: 上限値 * 割合 = 値

正規化の場合と同じで、下限が 0 でない場合はもう少し複雑な計算となる。

範囲が 20~100 で、割合が 0.5 の位置、つまり50%の位置にある値が 100 * 0.5 = 50 だね、というのは明らかにおかしい。これは下限が 0 でないからだが、下限が 0 でないなら、下限が 0 になるように範囲の幅をずらしてしまえばいい。

つまり 20~100 の範囲を下限が 0 になるように、下限と上限から 20 を引き 0~80 の範囲に修正する。これで下限が 0 になったので、上記の線形補間の式 上限値 * 割合 に適用すると 80 * 0.5 = 40 となる。これは 0~80 の範囲の50%の位置であるので、実際には引いた 20 をもとに戻さなければならない。なので 40 + 20 = 60 が 20~100 の範囲の50%の位置の値になる。

下限値 + (上限値 - 下限値) * 割合

計算式がわかったので実際にコードにしてみよう。

function lerp(a, b, t) {
  return a + (b - a) * t;
}

線形補間の計算をする lerp() は、a~b の範囲の中で割合 t の位置にある値を返す。p5.jsには同名同機能の関数が用意されているので今後はそちらを使用する。

正規表現の場合は norm(v, a, b) と、範囲の指定が後に来るのに対して、lerp() は先に範囲を指定しなければならないことに注意する。

a + (b - a) * 0 = a; // t = 0 の場合
a + (b - a) * 1 = b; // t = 1 の場合

線形補間の計算では t0 に近づくほど a に、 1 に近づくほど b に近い値が返る。

const x = lerp(a.x, b.x, t);
const y = lerp(a.y, b.y, t);

ab が点であり、割合 t の値を少しずつ増やして、軸ごとに線形補間をすると、a から b に向かって、2点の間をまっすぐ進むという特徴がある。これはオブジェクトに動きを付ける基本の式となるので是非覚えておきたい。

線形補間の式の変形

// 線形補間の式
v = a + (b - a) * t

// 両辺からaを引く
v - a = (b - a) * t

// 両辺を(b - a)で割る
(v - a) / (b - a) = t

// 左辺と右辺を入れ替える
t = (v - a) / (b - a)

線形補間の式を t について解く形に変形すると、正規化の式となる。

// 線形補間の式
v = a + (b - a) * t

// 右辺を展開する
v = a + b * t - a * t

// 右辺を並び替える
v = a - a * t + b * t

// 右辺をaで括る
v = a * (1 - t) + b * t

この変形して得られた (1 - t) : t の比でブレンドする線形補間の形もよく利用される。

線形補間を利用した作例

let route, t, i;

function setup() {
  createCanvas(windowWidth, windowHeight);
  
  // 点の座標
  route = [
    { x: 100, y: 100 },
    { x: 300, y: 300 },
    { x: 300, y: 100 },
  ];

  t = 0;
  i = 0;
}

function draw() {
  clear();

  route.forEach((r) => {
    circle(r.x, r.y, 20);
  });

  // prev~nextまでを線形補間して、tの位置に円を移動させる
  const prev = route[i];
  const next = route[(i + 1) % route.length];

  const x = lerp(prev.x, next.x, t);
  const y = lerp(prev.y, next.y, t);

  circle(x, y, 10);

  t += 0.01;
  if (t > 1) {
    t = 0;
    i++;
    i %= route.length;
  }
}

点を入れた配列 route を用意して、route[i] ~ route[i + 1] までを線形補間して円を移動させる。t1 を超えたら、今のルート間を移動し終えたということで、i の値を増やし、移動ルートを変える。

マップ

範囲の中にある値の位置を t として、別の範囲での位置 t にある値を返す機能をマップと呼ぶ。

図を見てみよう。2つの範囲があり、上側は 100~200、下側は 300~500 だ。ここで、上側で 140 にあたる位置が、下側だとどんな値になるのかが知りたい。

まず上側の範囲で 140 の位置の割合を計算する。

(140 - 100) / (200 - 100) = 0.4

正規化を利用して40%の位置にあることがわかった。次に下側の範囲で40%の位置にある値を計算をする。

300 + (500 - 300) * 0.4 = 380

線形補間を利用して 380 ということがわかった。これがマップの計算で、正規化と線形補間を利用した、ある範囲から別の範囲に値を変換するための関数になる。

範囲Bの値 = 線形補間(範囲Bの下限, 範囲Bの上限, 正規化(範囲Aの値, 範囲Aの下限, 範囲Aの上限))

計算式がわかったので実際にコードにしてみよう。

function map(v, a, b, c, d) {
  return lerp(c, d, norm(v, a, b));
}

マップ関数である map() は、範囲 a~b の中で v がどの位置にあるかを計算し、範囲 c~d で同じ位置にある値を返す。

p5.jsには同名同機能の関数が用意されているので、今後はそちらを使用する。

マップの利用例

HSVの色相の最大値は 359 だが、そうではない環境も度々見かける。たとえば 1001.0、果ては 255 で持つライブラリと遭遇したこともある。

今、色相の値を持っているが、これから利用するライブラリでは色相の範囲が違うという場合は、マップを利用すればいい。

// 0~359の範囲の色相を、0~100の範囲の色相に変換する
const newH = map(h, 0, 359, 0, 100);

範囲元/範囲先の下限が 0 なので、h / 359 * 100 と簡潔に記述できるので、わざわざマップ関数を利用する必要はないが、汎用性がある書き方なので覚えておくといいだろう。

マップの利用例 その2

const min = 200;
const max = 100;

norm(150, min, max); // 0.5
lerp(min, max, 0.5); // 150

正規化や線形補間では、範囲が 200~100 のような、min > max の範囲でも正しく動作する。

map(10, 0, 100, 100, 0); // 90
map(10, 100, 0, 0, 100); // 90

これはマップでも同様で、たとえば 0~100 の範囲を 100~0 の範囲に変換することができる。

つまり、値が大きくなるにつれて値を小さくする、もしくはその逆のような関数が簡単に作れるわけだ。

const r = map(x, 0, width, 300, 0);

円のx座標が大きくなるほど、円の半径が小さくなる作例。

const max = dist(0, 0, width / 2, height / 2);
const d = dist(width / 2, height / 2, mouseX, mouseY);
const r = map(d, 0, max, max, 0)

マウスが画面中央に近づくほど、円の半径が大きくなる作例。

範囲外にある値

norm(0, 100, 200); // -1
norm(300, 100, 200); // 2

lerp(100, 200, -1); // 0
lerp(100, 200, 2); // 300

map(-1, 0, 1, 100, 200); // 0
map(2, 0, 1, 100, 200); // 300

渡した範囲より外側にある値を指定した場合は計算結果も範囲外の値になる。

たとえば画面の中のマウス座標を正規化する場合、マウスが画面外に行くこともあり得るので、正規化の結果も 0 未満や 1 より大きい値になることがある。

その場合、範囲外の値はカットして、かならず範囲内の値を返すという方法があるが、それについては次の章で扱う。