jsdo.itにヒストグラムを表示するサンプルをアップしました。
ヒストグラムとは?
Wikipediaから引用すると、
ヒストグラム(英: histogram[1])とは、縦軸に度数、横軸に階級をとった統計グラフの一種で、データの分布状況を視覚的に認識するために主に統計学や数学、画像処理等で用いられる。柱図表[1]、度数分布図、柱状グラフともいう。
という、柱状グラフのことを指します。
Photoshopなどではおなじみですね。
ヒストグラムの拡張
画像ヒストグラムは、画像内のRGB値とLuminance(輝度)の分布状況を示してくれます。
上記のグラフはまさにRGBとLuminanceの、とある画像の分布状況です。
比較的よく見るものだと思います。
どう活用する?
Photoshopを触ったことがある人はイメージしやすいと思いますが、画像のコントラストを調整したりすることにも用いられています。
それが ヒストグラムの拡張 です。
Photoshopでいうと「レベル補正」で表示されるアレですね。
レベル補正を行うと画像のコントラストを調整することができます。
プログラムにする
今回の記事は、こちらを参考にさせて頂きました。
上記記事を元に、JSで作ったものがこちらです。
ヒストグラムを作る
ヒストグラムを作っているコードを抜粋すると以下になります。
やっていることはRGBそれぞれの値をヒストグラムとして分解し、さらにRGB値からLuminance値を算出して、それもヒストグラムにしています。
色情報は通常、RGBそれぞれが0〜255の256段階(8bit)で表現されます。(RGBA全部で32bit)
つまり、ピクセルに含まれる色成分は必ず0〜255の間に収まります。
なのでそれぞれのヒストグラム用に256の長さの配列を用意し、今調べているピクセルの情報を分解し、どこに分布しているのかをインクリメントしていく、という方法でヒストグラムを作成しています。
(ちなみに、ヒストグラムのひとつの柱を「ビン」というようです)
function getHistogram() {
var redHist = new Array(256);
var greenHist = new Array(256);
var blueHist = new Array(256);
var lumHist = new Array(256);
// 配列の初期化。すべてを一旦0にする。
for (var i = 0; i < 256; i++) {
redHist[i] = 0;
greenHist[i] = 0;
blueHist[i] = 0;
lumHist[i] = 0;
}
var imageData = ctx.getImageData(0, 0, cv.width, cv.height);
// 輝度値計算用
var cr = 0.298912;
var cg = 0.586611;
var cb = 0.114477;
var data = imageData.data;
for (var i = 0, len = data.length; i < len; i += 4) {
var r = data[i + 0];
var g = data[i + 1];
var b = data[i + 2];
var l = Math.floor((r * cr) + (g * cg) + (b * cb)); // Luminanceの`l`
redHist[r]++;
greenHist[g]++;
blueHist[b]++;
lumHist[l]++;
}
return {
redHist : redHist,
greenHist: greenHist,
blueHist : blueHist,
lumHist : lumHist
};
}
輝度値計算
コードを見ているとマジックナンバーがあることに気づくと思います。
var cr = 0.298912;
var cg = 0.586611;
var cb = 0.114477;
の部分ですね。
どうやらこれは、各色成分と計算することで輝度値を求めるもののようです。
cr, cg, cbはそれぞれRGBのどれに掛けるかの数字(割合)です。
なぜ均等にしないのでしょうか。
実は人間の目は緑色によく反応できるようになっているため、緑(cg)の割合が大きくなっている、というわけなんです。
(上記の数字全部を足すと1
になります)
輝度値についてはこちらの記事を参考にしました。
あとはこの数値を使って計算を行えば、輝度値が求まる、というわけです。
var l = Math.floor((r * cr) + (g * cg) + (b * cb)); // Luminanceの`l`
余談
ちなみに、シェーダのサンプルなどを見ていると以下のような式を目にする場合があります。
dot(Perception, color);
dot
、つまり内積ですね。
内積の計算方法は、まさに上記で書いた通りのものなのでそれを使っているのでしょう。
(あるいはPerception
とどれくらい似ているか、を計算している?)
ただ、やっていることは掛けて足しているだけです。
レベル補正を行う
さて、ヒストグラムを算出できたらそれを使って画像補正をしたいですね。
それが今回の記事のメインである「レベル補正」です。
サンプルを見てもらうと分かりますが、レベル(サンプル下端のドラッグできる▲)をいじると画像に変化が現れます。
左の三角がminレベルを、右の三角がmaxレベルを表しています。
これをどう使うかというと、min以下のレベルのものは強制的に0に、max以上のレベルのものはすべて255に変換し、中間は以下の式を用いて色の変換を行います。
dの範囲 | d1の値 |
---|---|
$d <= min$ | $d1 = 0$ |
$min < d < max$ | $d1 = (d - min) \cdot \frac{255}{max - min}$ |
$d >= max$ | $d1 = 255$ |
実際のプログラムは以下になります。
/**
* Spread RGB.
*
* @param {Number} min
* @param {Number} max
* @param {Array} src
* @param {Array} dst
*/
function spreadRGB(min, max, src, dst) {
var ratio = 255 / (max - min);
for (var i = 0, l = src.length; i < l; i += 4) {
dst[i + 3] = 255;
for (var j = 0; j < 3; j++) {
if (src[i + j] < min) {
dst[i + j] = 0;
}
else if (src[i + j] > max) {
dst[i + j] = 255;
}
else {
dst[i + j] = (src[i + j] - min) * ratio;
}
}
}
return dst;
}
min
とmax
はそれぞれレベル補正を行いたい範囲、src
は入力画像、dst
は入力画像と同じサイズのimagaDataのdata配列です。
あとは計算結果のimageData
配列をcanvas要素に適用してやれば、Photoshopのレベル補正に近い結果が得られます。