はじめに
アイデアが閃いて実現できないかと思い、Clade Codeと進めたことを記事にしようと思います
試していた感じ色を判定するのが非常に難しかったので、今回はどうやって色を認識させるのかの部分だけに絞ろうと思います
作りたいもの
スマホのカメラで撮影して、色を取得したい
HTMLとJavaScriptだけで、できれば動かしたい
デジタルで色を表現する方法
RGBとHSLという方法があります
- RGB
- 光の三原色である
Red(赤)、Green(緑)、Blue(青)を混ぜ合わせる「加法混色」に基づいたモデルです - 仕組み: それぞれの強さを0〜255の数値で表します
- 光の三原色である
- HSL
- 色を
色相(Hue)・彩度(Saturation)・輝度(Lightness)の3要素で表現するカラーモデル - 仕組み:
- H(色相): 0〜360度の角度(赤→黄→緑→青→紫)
- S(彩度): 0〜100%の鮮やかさ
- L(輝度): 0〜100%の明るさ
- 色を
色判定の方針:RGBではなくHSLで考える
色を判定・指定する際に、HSLは非常に適したカラーモデルです
RGB(赤・緑・青)が機械的な光の強度を数値化するのに対し、HSLは人間の視覚感覚(色相・彩度・明度)に近いため、直感的に色を理解・調整できます
例えば
カメラ映像の各ピクセルはRGBで取得できます
しかし「赤っぽいかどうか」をRGBで判定しようとすると
明るい赤 rgb(255, 80, 80) と暗い赤 rgb(120, 20, 20) は、同じ 赤 でも数値が全然違います
HSLで考える
HSL(色相・彩度・明度) に変換すると
| 要素 | 意味 | 範囲 |
|---|---|---|
| H (Hue) | 色の種類(赤・青など) | 0〜360° |
| S (Saturation) | 鮮やかさ | 0〜1 |
| L (Lightness) | 明るさ | 0〜1 |
HSLなら 赤 = 色相が0°付近 とシンプルに表現することができます
各色の定義を考えてみる
コードに落とし込んで考えてみます
const DEFS = {
red: { label:'赤', css:'#ff3333', hRanges:[[0,14],[346,360]], sMin:.45, lMin:.15, lMax:.85 },
orange: { label:'オレンジ', css:'#ff8800', hRanges:[[15,40]], sMin:.55, lMin:.25, lMax:.85 },
yellow: { label:'黄', css:'#ddcc00', hRanges:[[41,74]], sMin:.55, lMin:.35, lMax:.95 },
green: { label:'緑', css:'#22bb33', hRanges:[[75,160]], sMin:.30, lMin:.10, lMax:.80 },
cyan: { label:'水色', css:'#00bbcc', hRanges:[[161,200]], sMin:.45, lMin:.20, lMax:.80 },
blue: { label:'青', css:'#2255ff', hRanges:[[201,255]], sMin:.35, lMin:.10, lMax:.75 },
purple: { label:'紫', css:'#9922cc', hRanges:[[256,320]], sMin:.30, lMin:.10, lMax:.75 },
pink: { label:'ピンク', css:'#ff55aa', hRanges:[[321,345]], sMin:.45, lMin:.35, lMax:.90 },
};
各色に hRanges(色相の許容範囲)と sMin(最低彩度)、lMin/lMax(明度の範囲)を定義しました
赤だけ範囲が2つある理由:色相は円環なので、赤は0°付近と360°付近をまたぎます
[[0,14],[346,360]] という2つの範囲で表現する必要があります
彩度・明度の制限が重要:白・黒・グレーは色相がどんな値でも彩度がほぼ0になります
sMin で弾かないと誤検出が大量に出ます
RGB → HSL 変換
function rgbToHsl(r,g,b) {
r /= 255; g /= 255; b /= 255; //0〜1に正規化
const max = Math.max(r,g,b), min = Math.min(r,g,b);
leth=0, s=0, l=(max+min)/2; //明度
if(max !== min) {
const d = max-min; //彩度の分母
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); //彩度
switch(max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break; //赤が最大
case g:
h = ((b - r) / d + 2) / 6;
break; //緑が最大
case b:
h = ((r - g) / d + 4) / 6;
break; //青が最大
}
}
// H:0〜360度, S:0〜1, L:0〜1
return[ h * 360, s, l];
}
| ステップ | 計算 | 意味 |
|---|---|---|
| 正規化 | r /= 255 | 0〜255 → 0〜1 |
| 明度L | (max + min) / 2 | 最大と最小の中点 |
| 彩度S | d / (2 - max - min) or d / (max + min) | L > 0.5で式が変わる |
| 色相H | どのチャンネルが最大かで場合分け | 0〜1 → 最後に ×360 |
戻り値の h を × 360 して度数(0〜360) にしているのがポイントです
/ 6 で割っているのは、色相の計算が本来「0〜6の範囲」で出てくるためです
赤(0) → 黄(1) → 緑(2) → 水色(3) → 青(4) → 紫(5) → 赤(6)
これを 0〜1 に圧縮しておいて、最後に × 360 で度数に戻します
0〜1 → × 360 → 0〜360°
つまり:
h = rawH / 6 // 0〜6 を 0〜1 に圧縮
return [h * 360] // 0〜1 を 0〜360° に展開
この2ステップを合わせると rawH / 6 * 360 = rawH * 60 と同じで、色相環の1セクション = 60° という色彩論の定義と一致させることができます
ピクセルの色判定
function matches(r, g, b, name) {
const d = DEFS[name];
const [h, s, l] = rgbToHsl(r, g, b);
if (s < d.sMin || l < d.lMin || l > d.lMax) return false;
return d.hRanges.some(([lo, hi]) => h >= lo && h <= hi);
}
2段階で判定しています
- 彩度・明度で無彩色をまず除外する
- 色相が定義の範囲内かを確認する
まとめ
今回の色判定のポイントをまとめます
- RGBのまま扱わず、HSLに変換してから色相で判定する
- 赤のように0°をまたぐ色は 複数範囲 で定義する
- 彩度の下限を設けて 無彩色の誤検出を防ぐ

