LCH色空間入門 — ファッション配色分析に最適な理由
RGB・HSLでは配色の良し悪しを正確に数値化できない。LCH色空間なら「数値の距離=人間が感じる色の違い」になる。配色アルゴリズムに最適な色空間を、Pythonコード例とともに解説。
「赤いトップスと青いボトムスの配色は良いか?」——この問いに計算で答えるには、色を数値化し、その距離を測り、配色理論に照合するパイプラインが必要です。ここで最初の分岐点になるのが 色空間の選択 です。RGB や HSL では、数値上の距離と人間の知覚がずれるため、配色の調和を正確に測定できません。LCH 色空間を使えば「数値の距離 = 人間が感じる色の違い」になり、配色分析アルゴリズムの精度が根本から変わります。
この記事では、RGB・HSL・LCH の 3 つの色空間を比較し、なぜ配色分析には LCH が最適なのかを Python コード例付きで解説します。
なぜ色空間が重要なのか
配色分析のパイプラインは、大きく 3 ステップです。
- 色の数値化 — 画像からアイテムの代表色を抽出する
- 距離の計算 — 2 色間の「違い」を数値で求める
- 理論への照合 — 補色・類似色などのハーモニーパターンに当てはめる
ステップ 1 と 2 の精度は、使う色空間に完全に依存します。「距離」の定義が人間の知覚と合っていなければ、ステップ 3 でどんなに精密な理論を適用しても意味がありません。
配色分析の技術的な全体像については「AI はどうやって服の配色を分析するのか」で解説しています。本記事では、そのパイプラインの土台となる色空間の選択に焦点を当てます。
RGB の問題点
RGB(Red, Green, Blue)は、ディスプレイが色を表示するための仕組みです。3 つのチャンネルにそれぞれ 0〜255 の値を割り当て、光の三原色を混合して色を再現します。
プログラミングで最も馴染み深い色空間ですが、配色分析には致命的な欠点があります。
知覚との不一致を実験する
以下の Python コードで、RGB 上の「ユークリッド距離」と、人間の知覚的な差を比較してみます。
import math
def rgb_distance(c1: tuple, c2: tuple) -> float:
"""RGB空間でのユークリッド距離"""
return math.sqrt(sum((a - b) ** 2 for a, b in zip(c1, c2)))
# ペアA: 赤 vs 緑(誰が見ても全く違う色)
red = (255, 0, 0)
green = (0, 255, 0)
dist_a = rgb_distance(red, green)
# ペアB: 青 vs 少し明るい青(似たような色)
blue = (0, 0, 255)
light_blue = (0, 128, 255)
dist_b = rgb_distance(blue, light_blue)
print(f"ペアA(赤 vs 緑): {dist_a:.1f}") # 360.6
print(f"ペアB(青 vs 水色): {dist_b:.1f}") # 128.0
距離の比率は約 2.8 倍。しかし実際に目で見ると、赤と緑の差は「完全に別の色」であり、青と水色の差は「同系色のバリエーション」です。知覚的な差は 2.8 倍どころではなく、質的にまったく異なるレベルです。
もう一つの問題を見てみましょう。
# 同じRGB距離(128)なのに、知覚的な差がまったく違う例
dark_pair = rgb_distance((10, 10, 10), (10, 10, 138)) # 暗い黒 vs 暗い紺
light_pair = rgb_distance((200, 200, 200), (200, 200, 72)) # 明るいグレー vs 明るい黄
print(f"暗いペア: {dark_pair:.1f}") # 128.0
print(f"明るいペア: {light_pair:.1f}") # 128.0
RGB 距離は同じ 128 ですが、暗い領域での差は人間にはほとんど見分けがつかず、明るい領域での差ははっきり分かります。RGB は人間の非線形な色覚特性を反映していないのです。
RGB が配色分析に不向きな根本原因
RGB は デバイスの発光特性 に合わせた座標系であり、人間の視覚系のモデルではありません。
- 3 軸(R, G, B)が知覚的に独立していない
- 同じ距離でも、色空間内の位置によって知覚差が変わる
- 色相・彩度・明度という「人間が色を認識する 3 要素」に直接対応しない
HSL/HSV の改善と限界
HSL(Hue, Saturation, Lightness)は、RGB の欠点を改善するために考案された色空間です。
import colorsys
def rgb_to_hsl(r, g, b):
"""RGB(0-255) -> HSL(H:0-360, S:0-100, L:0-100)"""
r_, g_, b_ = r / 255, g / 255, b / 255
h, l, s = colorsys.rgb_to_hls(r_, g_, b_)
return round(h * 360, 1), round(s * 100, 1), round(l * 100, 1)
print(rgb_to_hsl(255, 0, 0)) # (0.0, 100.0, 50.0) 赤
print(rgb_to_hsl(0, 255, 0)) # (120.0, 100.0, 50.0) 緑
print(rgb_to_hsl(0, 0, 255)) # (240.0, 100.0, 50.0) 青
改善点:
- 色相(H)が角度で表現される — 0°〜360° の円周上に色が並び、色相環にそのまま対応する。補色は 180° 反対、類似色は 30° 以内、という直感的な判定が可能
- 彩度と明度が分離されている — 「鮮やかさ」と「明るさ」を独立に扱える
残る限界:
しかし HSL の彩度(S)と明度(L)は 知覚的に均等ではありません。
# HSL で同じ彩度100%、明度50% でも、知覚的な明るさが全然違う
colors = {
"黄色": rgb_to_hsl(255, 255, 0), # H:60, S:100, L:50
"青": rgb_to_hsl(0, 0, 255), # H:240, S:100, L:50
}
for name, hsl in colors.items():
print(f"{name}: H={hsl[0]}° S={hsl[1]}% L={hsl[2]}%")
黄色と青は HSL 上では S と L が同じ値ですが、黄色は明るく感じ、青は暗く感じます。これは人間の錐体細胞の感度特性によるもので、HSL の明度はこの生理的特性を反映していません。
つまり HSL で「明度差が 20 あるから十分なコントラスト」と判定しても、色相によっては全くコントラストが出ていない、ということが起こります。配色の「メリハリ」を正確に評価できない色空間では、実用的な配色分析は困難です。
LCH 色空間の特長
LCH(Lightness, Chroma, Hue)は、CIELAB 色空間を極座標表現に変換したものです。CIE(国際照明委員会)が人間の色覚実験データに基づいて設計した色空間であり、知覚均等性 を最大の設計目標としています。
3 つの軸
| 軸 | 意味 | 範囲 | 対応する知覚 |
|---|---|---|---|
| L (Lightness) | 知覚明度 | 0(黒)〜 100(白) | どれだけ明るく見えるか |
| C (Chroma) | 知覚彩度 | 0(無彩色)〜 約 150 | どれだけ鮮やかに見えるか |
| H (Hue) | 色相角 | 0°〜 360° | 何色に見えるか |
知覚均等性とは
LCH の核心的な特長は、任意の 2 色間のユークリッド距離(ΔE)が、人間が感じる色の違いの大きさに近似的に比例する ことです。
- ΔE < 1: ほとんどの人が区別できない
- ΔE 1〜3: 注意深く比べれば分かる
- ΔE 3〜5: 一目で分かる差
- ΔE > 10: 明らかに別の色
RGB ではこの対応関係が成り立ちませんでした。LCH なら「2 色の距離が 5 だから、はっきり違うと感じる程度の差」と定量的に言えます。
HSL との決定的な違い
先ほどの黄色と青の問題を LCH で確認してみましょう。
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_conversions import convert_color
import math
def rgb_to_lch(r, g, b):
"""RGB(0-255) -> LCH"""
rgb = sRGBColor(r / 255, g / 255, b / 255)
lab = convert_color(rgb, LabColor)
L = lab.lab_l
a, b_val = lab.lab_a, lab.lab_b
C = math.sqrt(a**2 + b_val**2)
H = math.degrees(math.atan2(b_val, a)) % 360
return round(L, 1), round(C, 1), round(H, 1)
yellow = rgb_to_lch(255, 255, 0)
blue = rgb_to_lch(0, 0, 255)
print(f"黄色: L={yellow[0]}, C={yellow[1]}, H={yellow[2]}°")
# 黄色: L=97.1, C=96.9, H=102.9°
print(f"青: L={blue[0]}, C={blue[1]}, H={blue[2]}°")
# 青: L=32.3, C=133.8, H=306.3°
HSL では同じ明度 50% だった黄色と青が、LCH では L=97.1 と L=32.3 と 大きく異なる値 になります。これが正しい知覚です。黄色は実際に明るく見え、青は暗く見える。LCH はこの人間の知覚を正しく反映しています。
CSS の oklch() — フロントエンドとの接点
LCH の実用性は、CSS Color Level 4 での oklch() 関数サポートによってフロントエンド開発にも波及しています。
/* oklch(Lightness Chroma Hue) */
.primary {
color: oklch(0.65 0.15 250); /* 知覚的に均等な明度で色を指定 */
}
/* 明度だけ変えてホバー効果 — 知覚的に均等な明暗変化 */
.button {
background: oklch(0.55 0.2 250);
}
.button:hover {
background: oklch(0.65 0.2 250); /* L を上げるだけ */
}
OKLCH は CIELAB ベースの LCH をさらに改良した色空間で、青〜紫領域での色相シフト問題を修正しています。バックエンド(Python)で CIELAB LCH を使い、フロントエンド(CSS)で OKLCH を使う、という技術スタック構成は理にかなっています。
配色分析での具体的な使い方
LCH の 3 軸は、配色分析の各評価基準にそのまま対応します。
色相角差 → ハーモニーパターン判定
LCH の H(色相角)は 0°〜360° の角度です。2 色の色相角差から、配色理論のどのパターンに該当するかを判定できます。
def hue_diff(h1: float, h2: float) -> float:
"""2つの色相角の最小差(0-180°)を返す"""
diff = abs(h1 - h2) % 360
return min(diff, 360 - diff)
def classify_harmony(h1: float, h2: float) -> str:
"""色相角差からハーモニーパターンを判定"""
diff = hue_diff(h1, h2)
if diff <= 30:
return "類似色(Analogous)"
elif 30 < diff <= 60:
return "類似補色"
elif 150 <= diff <= 180:
return "補色(Complementary)"
elif 110 <= diff <= 130:
return "トライアド候補"
else:
return "その他"
# 実例: ネイビー × ベージュ
navy = rgb_to_lch(44, 95, 138) # H ≈ 265°
beige = rgb_to_lch(212, 165, 116) # H ≈ 71°
diff = hue_diff(navy[2], beige[2])
print(f"色相角差: {diff:.1f}° → {classify_harmony(navy[2], beige[2])}")
# 色相角差: 約166° → 補色(Complementary)
ネイビーとベージュは色相角差が約 166° で、補色に近い関係。配色理論上、お互いを引き立て合う効果的な組み合わせです。こうした判定の詳細は「補色・類似色・トライアドを服で使いこなす実践ガイド」で具体的なコーディネート例とともに解説しています。
彩度差チェック → 「浮いて見える」検出
def check_chroma_balance(c1: float, c2: float, threshold: float = 40) -> str:
"""彩度差が大きすぎると片方が浮いて見える"""
diff = abs(c1 - c2)
if diff > threshold:
return f"警告: 彩度差 {diff:.1f} — 片方が浮いて見える可能性"
return f"OK: 彩度差 {diff:.1f}"
# 鮮やかなコーラルピンク vs くすんだカーキ
coral = rgb_to_lch(255, 127, 80) # C ≈ 65
khaki = rgb_to_lch(189, 183, 107) # C ≈ 35
print(check_chroma_balance(coral[1], khaki[1]))
色相が補色関係でも、彩度差が大きいと「合わない」と感じる原因になります。HSL の彩度ではこの差を正確に測定できませんが、LCH の C 値なら知覚に即した判定が可能です。
明度コントラスト → メリハリ評価
def check_lightness_contrast(l1: float, l2: float) -> str:
"""明度差でコーディネートのメリハリを評価"""
diff = abs(l1 - l2)
if diff < 10:
return f"のっぺり: 明度差 {diff:.1f} — メリハリ不足"
elif diff < 30:
return f"適度: 明度差 {diff:.1f} — 自然なコントラスト"
else:
return f"強い: 明度差 {diff:.1f} — 高コントラスト"
# ミディアムグレー vs ミディアムブルー
gray = rgb_to_lch(128, 128, 128) # L ≈ 54
m_blue = rgb_to_lch(100, 130, 180) # L ≈ 54
print(check_lightness_contrast(gray[0], m_blue[0]))
# のっぺり: 明度差 ≈ 0 — メリハリ不足
LCH の L 値は知覚明度なので、「明度差 10 以下はメリハリ不足」というルールが色相に関係なく一貫して適用できます。HSL ではこの一貫性が保証されませんでした。
RGB → LCH 変換の実装
実装に入る前に、変換チェーンの全体像を把握しておきましょう。
RGB → sRGB Linear → XYZ → Lab(CIELAB) → LCH
各ステップの役割:
- RGB → sRGB Linear: ガンマ補正を外す(sRGB はガンマ 2.2 相当のカーブがかかっている)
- sRGB Linear → XYZ: CIE XYZ 色空間への線形変換(3x3 行列)
- XYZ → Lab: 人間の知覚に合わせた非線形変換(立方根関数)
- Lab → LCH: 直交座標(a, b)から極座標(C, H)への変換
Python での実装
自前実装する必要はありません。colour-science ライブラリが高精度な変換を提供しています。
# pip install colour-science
import colour
import numpy as np
def rgb_to_lch_colour(r, g, b):
"""colour-science ライブラリを使った高精度変換"""
rgb = np.array([r, g, b]) / 255
xyz = colour.sRGB_to_XYZ(rgb)
lab = colour.XYZ_to_Lab(xyz)
lch = colour.Lab_to_LCHab(lab)
return {
"L": round(lch[0], 2),
"C": round(lch[1], 2),
"H": round(lch[2], 2)
}
# ファッションでよく使う色を変換
colors = {
"ネイビー": (44, 95, 138),
"ベージュ": (212, 165, 116),
"チャコールグレー": (54, 69, 79),
"テラコッタ": (204, 78, 34),
"オリーブ": (128, 128, 0),
}
for name, rgb in colors.items():
lch = rgb_to_lch_colour(*rgb)
print(f"{name:12s}: L={lch['L']:6.2f} C={lch['C']:6.2f} H={lch['H']:6.2f}°")
軽量な実装なら colormath も選択肢です。
# pip install colormath
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_conversions import convert_color
rgb = sRGBColor(44/255, 95/255, 138/255)
lab = convert_color(rgb, LabColor)
# lab.lab_l, lab.lab_a, lab.lab_b から LCH を計算
Web フロントエンドなら CSS oklch()
フロントエンドで LCH を使う場合、JavaScript での変換を自前実装する必要はありません。CSS Color Level 4 の oklch() を直接使えます。
:root {
--navy: oklch(0.45 0.08 250);
--beige: oklch(0.75 0.06 80);
--terracotta: oklch(0.55 0.15 40);
}
/* 配色パレットとして利用 */
.card-primary { background: var(--navy); }
.card-secondary { background: var(--beige); }
.card-accent { background: var(--terracotta); }
2026 年現在、主要ブラウザ(Chrome, Firefox, Safari, Edge)すべてが oklch() をサポートしています。
まとめ:なぜ私たちは LCH を選んだか
色空間の選択は、配色分析の精度を左右する最も根本的な設計判断です。
| 色空間 | 色相の直感性 | 知覚均等性 | 配色分析への適性 |
|---|---|---|---|
| RGB | なし | なし | 不向き |
| HSL/HSV | 高い | 部分的 | 限定的 |
| LCH (CIELAB) | 高い | 高い | 最適 |
| OKLCH | 高い | 非常に高い | 最適(特に青紫領域) |
私たちが magicoord の配色分析エンジンで LCH を採用したのは、実装を進める中で RGB や HSL ベースの距離計算が「明らかにおかしい」結果を返すケースに何度も直面したからです。特に、暗い色同士のコーディネート(ネイビー × チャコール × ダークブラウンなど)で、RGB ベースでは差が出ているのに人間にはほぼ同じに見える、という問題が頻発しました。
LCH に切り替えたことで、色相・彩度・明度それぞれの軸で知覚に基づいた評価ができるようになり、ハーモニー判定の精度が大幅に向上しました。
配色理論の基礎を学びたい方は「配色理論 × AI — 服の色選びを「感覚」から「理論」に変える」を、AI が配色を分析する全体の仕組みを知りたい方は「AI はどうやって服の配色を分析するのか」をご覧ください。
この記事は wizPulseAI 編集部が、AI ツールの支援を受けて作成しました。
よくある質問(FAQ)
Q: LCH と OKLCH はどう違いますか?どちらを使うべきですか? A: LCH は CIELAB ベース、OKLCH は Oklab ベースの極座標表現です。OKLCH は CIELAB の青〜紫領域での色相シフト問題(彩度を変えると色相が微妙にずれる)を改善しています。Python でバックエンド処理するなら CIELAB LCH(ライブラリが充実)、CSS でフロントエンド表示するなら OKLCH(ブラウザネイティブ対応)、という使い分けが実用的です。私たちの magicoord でもこの構成を採用しています。
Q: ΔE の計算式は CIE76 と CIE2000 のどちらを使うべきですか?
A: CIE76(単純なユークリッド距離)は実装が簡単ですが、特に彩度が低い領域で精度が落ちます。CIE2000(CIEDE2000)は明度・彩度・色相それぞれに重み付け関数を導入しており、全領域でより正確な知覚差を返します。配色分析のように精度が求められる用途では CIEDE2000 を推奨します。colour-science ライブラリの colour.delta_E() で簡単に切り替えられます。
Q: 配色分析以外に LCH が役立つ場面はありますか? A: UI デザインのカラーパレット生成で威力を発揮します。LCH の L 値を固定して H を変えれば、「知覚的に同じ明るさの異なる色相」が得られます。アクセシビリティ(WCAG コントラスト比)の計算でも、知覚明度に基づく LCH は RGB より信頼性の高い結果を返します。
