wizPulseAI
AI開発実践

LCH色空間入門 — ファッション配色分析に最適な理由

RGB・HSLでは配色の良し悪しを正確に数値化できない。LCH色空間なら「数値の距離=人間が感じる色の違い」になる。配色アルゴリズムに最適な色空間を、Pythonコード例とともに解説。

wizPulseAI 編集部
12分
#LCH#色空間#配色分析#Python#カラーサイエンス

「赤いトップスと青いボトムスの配色は良いか?」——この問いに計算で答えるには、色を数値化し、その距離を測り、配色理論に照合するパイプラインが必要です。ここで最初の分岐点になるのが 色空間の選択 です。RGB や HSL では、数値上の距離と人間の知覚がずれるため、配色の調和を正確に測定できません。LCH 色空間を使えば「数値の距離 = 人間が感じる色の違い」になり、配色分析アルゴリズムの精度が根本から変わります。

この記事では、RGB・HSL・LCH の 3 つの色空間を比較し、なぜ配色分析には LCH が最適なのかを Python コード例付きで解説します。

なぜ色空間が重要なのか

配色分析のパイプラインは、大きく 3 ステップです。

  1. 色の数値化 — 画像からアイテムの代表色を抽出する
  2. 距離の計算 — 2 色間の「違い」を数値で求める
  3. 理論への照合 — 補色・類似色などのハーモニーパターンに当てはめる

ステップ 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

各ステップの役割:

  1. RGB → sRGB Linear: ガンマ補正を外す(sRGB はガンマ 2.2 相当のカーブがかかっている)
  2. sRGB Linear → XYZ: CIE XYZ 色空間への線形変換(3x3 行列)
  3. XYZ → Lab: 人間の知覚に合わせた非線形変換(立方根関数)
  4. 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 より信頼性の高い結果を返します。