フォトショになりたい Part 2: 色調補正

## 色調補正
- HSV
    - 色相(Hue)
    - 彩度(Saturation)
    - 明度(Value/Brightness)
    - 自然な彩度
- ホワイトバランス
    - 色温度(temperature)
    - 色被り補正(deviation)

## HSV
RGBとHSVは一つの色を別の座標空間で表現しているだけ。
変換式は[RGBとHSV・HSBの相互変換ツールと変換計算式](https://www.peko-step.com/tool/hsvrgb.html#ppick2)より。丸め誤差を除けば一応全単射なはず。
(以下の計算は、それぞれ0 to 255に正規化しています)。

### 色相
RGBによって貼られる平行六面体を$r+g+b$ベクトルから見たときの偏角
(詳しくは[RGB正六面体 → HSL円柱](http://design0.w4c.work/status/419)を参照)

0-360度にするとImageData型(Uint8ClampedArray)に入りきらないので、0から255に納めている。

```Javascript:calcHue.js
function calcHue(r, g, b) { // 0 to 255
  const MAX = Math.max(r, g, b);
  const MIN = Math.min(r, g, b);
  if (MAX === MIN) {
    return 0;
  } if (MIN === b) {
    const h = 42.5 * ((g - r) / (MAX - MIN)) + 42.5;
    return h > 0 ? h : h + 255;
  } if (MIN === r) {
    const h = 42.5 * ((b - g) / (MAX - MIN)) + 127.5;
    return h > 0 ? h : h + 255;
  }
  const h = 42.5 * ((r - b) / (MAX - MIN)) + 212.5;
  return h > 0 ? h : h + 255;
}
```

### 彩度
RGBの最大値と最小値の比。

```Javascript:calcSaturation.js
function calc(r, g, b) { // 0 to 255
  const MAX = Math.max(r, g, b);
  const MIN = Math.min(r, g, b);
  return (MAX - MIN) / MAX * 255;
}
```

### 明度
RGBの最大値。

```Javascript:calcBrightness.js
const calcBrightness = (r, g, b) => Math.max(r, g, b);
```


### まとめると
```Javascript:hsvCvt.js
function rgb2hsv(img) { // H: 0~255, S: 0~255, V: 0~255
  for (let i = 0; i < img.data.length; i += 4) {
    const r = img.data[i];
    const g = img.data[i + 1];
    const b = img.data[i + 2];
    const MAX = Math.max(r, g, b);
    const MIN = Math.min(r, g, b);
    img.data[i] = calcHue(r, g, b);
    img.data[i + 1] = (MAX - MIN) / MAX * 255;
    img.data[i + 2] = MAX;
  }
  return img;
}

function hsv2rgb(img) {
  for (let i = 0; i < img.data.length; i += 4) {
    const h = img.data[i];
    const s = img.data[i + 1];
    //MAX = v
    const MAX = img.data[i + 2];
    const MIN = MAX - ((s / 255) * MAX);
    if (h < 42.5) {
      img.data[i] = MAX;
      img.data[i + 1] = (h / 42.5) * (MAX - MIN) + MIN;
      img.data[i + 2] = MIN;
    } else if (h < 85) {
      img.data[i] = ((85 - h) / 42.5) * (MAX - MIN) + MIN;
      img.data[i + 1] = MAX;
      img.data[i + 2] = MIN;
    } else if (h < 127.5) {
      img.data[i] = MIN;
      img.data[i + 1] = MAX;
      img.data[i + 2] = ((h - 85) / 42.5) * (MAX - MIN) + MIN;
    } else if (h < 170) {
      img.data[i] = MIN;
      img.data[i + 1] = ((170 - h) / 42.5) * (MAX - MIN) + MIN;
      img.data[i + 2] = MAX;
    } else if (h < 212.5) {
      img.data[i] = ((h - 170) / 42.5) * (MAX - MIN) + MIN;
      img.data[i + 1] = MIN;
      img.data[i + 2] = MAX;
    } else {
      img.data[i] = MAX;
      img.data[i + 1] = MIN;
      img.data[i + 2] = ((255 - h) / 42.5) * (MAX - MIN) + MIN;
    }
  }
  return img;
}
```

### 自然な彩度
彩度にトーンカーブをかける
→すでに彩度が高いところはそのままで、低いところを上げたい
→とりあえず**両対数スケール**で実装
(詳しくは[スケール対数曲線をトーンカーブとする画像補正](https://mikio.hatenablog.com/entry/2018/11/16/015606)を参照)

```Javascript:calcSaturation.js
const natural_s = (s, k) => log_log_scale(s, k);
```

### HSV系全体の流れ
1. RGBからHSVに変換
2. HSV空間でいじる
3. HSVからRGBに変換

```Javascript:editHSV.js
function editHSV(img, h_rotation, s_magnification, natural_s, v_magnification) {
  rgb2hsv(img);
  hsvCvt(img, h_rotation, s_magnification, natural_s, v_magnification);
  hsv2rgb(img);
  return img;
}
```

## ホワイトバランス
こちらはRGB空間を弄る。
RGBそれぞれの感度の違いを再現するので、普通にRGBそれぞれを定数倍すればいいだけだと思う。

ただ、Photoshopには以下のスライダーがあったのでそれだけ実装した。

### 色温度
[基本がわかる！色かぶりの補正【1】ホワイトバランスと色温度](https://psgips.net/photoshop-reference/color-cast-1)
多分**青いか黄色いか**という話なので、とりあえず$B$チャンネルをk倍した。


### 色被り補正
Photoshopの色被り補正は**緑-マゼンタの軸**で補正しているように見えるので、$G$チャンネルを定数倍した。
理論上これと露出をいじれば全パターン再現できているはず。

## まとめ
- HSV系は変換がめんどくさいが、HSV空間に持ち込んで色々いじった後RGBに戻す
- 色温度・色被り補正はホワイトバランスをいじることに相当する