色調補正
- HSV
- 色相(Hue)
- 彩度(Saturation)
- 明度(Value/Brightness)
- 自然な彩度
- ホワイトバランス
- 色温度(temperature)
- 色被り補正(deviation)
HSV
RGBとHSVは一つの色を別の座標空間で表現しているだけ。
変換式はRGBとHSV・HSBの相互変換ツールと変換計算式より。丸め誤差を除けば一応全単射なはず。
(以下の計算は、それぞれ0 to 255に正規化しています)。
色相
RGBによって貼られる平行六面体を$r+g+b$ベクトルから見たときの偏角
(詳しくはRGB正六面体 → HSL円柱を参照)
0-360度にするとImageData型(Uint8ClampedArray)に入りきらないので、0から255に納めている。
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の最大値と最小値の比。
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の最大値。
const calcBrightness = (r, g, b) => Math.max(r, g, b);
まとめると
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;
}
自然な彩度
彩度にトーンカーブをかける
→すでに彩度が高いところはそのままで、低いところを上げたい
→とりあえず両対数スケールで実装
(詳しくはスケール対数曲線をトーンカーブとする画像補正を参照)
const natural_s = (s, k) => log_log_scale(s, k);
HSV系全体の流れ
- RGBからHSVに変換
- HSV空間でいじる
- HSVからRGBに変換
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】ホワイトバランスと色温度
多分青いか黄色いかという話なので、とりあえず$B$チャンネルをk倍した。
色被り補正
Photoshopの色被り補正は緑-マゼンタの軸で補正しているように見えるので、$G$チャンネルを定数倍した。
理論上これと露出をいじれば全パターン再現できているはず。
まとめ
- HSV系は変換がめんどくさいが、HSV空間に持ち込んで色々いじった後RGBに戻す
- 色温度・色被り補正はホワイトバランスをいじることに相当する