IDW補間で実現するRGB→マンセル変換 3方式比較と実装の全手順

2026年2月23日

RGBとマンセルを繋ぐ技術的な壁

「#C8867A をマンセル表記にしてほしい」と言われたとき、手作業で対応表を引いた経験があるだろうか。RGBはデバイス依存の3チャネル表現、マンセルは人間の知覚に基づく色相・明度・彩度の三次元空間。この二者の間には数学的に一対一の逆変換式が存在しない。CIE XYZやCIELABを経由すれば理論的にはマッピング可能だが、ブラウザ上で動く軽量ツールとして「計算コスト」「外部依存ゼロ」「実用的な精度」を同時に満たすのが本質的な難所となる。

さらに、日塗工(JPMA)コードへの変換も求められる場面では、離散的な色見本群との距離計算が加わる。色相環は360度で循環するため、単純な数値平均では350度と10度の平均が180度になるという致命的な誤差が生じる。

この記事では、3つの候補方式を比較したうえで、逆距離加重補間(IDW)を採用した実装の全手順――格子点距離の算出、色相角のベクトル平均、ゼロ除算回避、WCAG相対輝度との前処理共有――を数値例付きで示す。

候補方式の比較

RGB→マンセル変換の実現手段として、以下の3方式を検討した。

| 方式 | 精度 | ブラウザ実装性 | 外部依存 | 主な弱点 | |------|------|---------------|---------|---------| | ICCプロファイル変換 | 高い | 低い | ライブラリ必須 | ブラウザ間のICC対応差 | | 逆距離加重補間(IDW) | 中(格子密度依存) | 高い | なし | 格子外の近似誤差 | | 回帰モデル(機械学習) | データ次第 | 中 | 学習済みモデル | モデルサイズ・精度保証 |

ICCプロファイル変換

CIE XYZやCIELABを経由し、マンセル・カラー・システムの定義に基づいて厳密に変換する方式。印刷やプロフェッショナル用途での再現性は高い。しかし、ブラウザごとにICCプロファイルのバージョン(v2/v4)対応が異なり、レンダリング意図(perceptual / relative colorimetric)の解釈も一致しない。クライアント側でプロファイルを解釈するにはLittleCMSなどのライブラリに依存する必要があり、バンドルサイズが膨らむ。外部依存なしでブラウザ完結という要件を満たせないため不採用とした。

逆距離加重補間(IDW)

マンセル格子点(40色相 x 複数明度・彩度の代表RGB値)をあらかじめ用意し、入力RGBからの距離に反比例する重みで補間する方式。格子点のRGB値さえ確定していれば純粋なJavaScriptだけで実装でき、外部ライブラリは不要。計算量は格子点数Nに対してO(N)の単一走査で完結するため、ユーザー操作への応答性も良い。弱点は格子の離散性に由来する近似誤差と、色相角の循環処理が必要になる点。

回帰モデル

RGB→マンセルの対応データセットで回帰モデルを学習させる方式。十分な学習データがあれば高精度を期待できるが、モデルサイズがブラウザ配信に適さない場合がある。さらに、学習データに含まれない色域外の入力に対する精度保証が難しく、エッジケースでの挙動がブラックボックスになりやすい。説明性と軽量性の両面で採用を見送った。

なぜIDWを選んだか

決め手は「ブラウザ完結性」と「計算の透明性」の両立。ICCは理論的に優れるがWeb環境での一貫性が保証できない。回帰モデルはブラックボックス性が高い。IDWなら「近い格子点ほど影響が大きい」という直感的な原理で動き、格子点データと距離の重みという2つの要素だけで結果を説明できる。格子外の高彩度領域にはクランプ処理を設けることで実用上の問題を低減している。

実装の詳細

計算フロー

全体の処理は6ステップで構成される。

  1. 入力sRGB(8bit)を受け取る
  2. 格子点集合(MUNSELL_GRID)全点とのsRGB空間上のユークリッド距離を算出する
  3. 距離の小さい上位3点を抽出する
  4. 逆距離加重(IDW)で色相角・明度・彩度を補間する
  5. 補間した色相角から最寄りのマンセル色相表記に変換する
  6. WCAGコントラスト比を算出する(sRGBガンマ逆変換の前処理を共有)

距離計算と逆距離加重

入力RGB = (R, G, B) と格子点 GP_i = (R_i, G_i, B_i) のユークリッド距離を求め、距離の逆数を重みとする。

入力: RGB = (198, 134, 120)

格子点との距離計算:
  GP_A (5YR 7/6): RGB_A = (207, 165, 128)
    d_A = sqrt((198-207)² + (134-165)² + (120-128)²)
        = sqrt(81 + 961 + 64)
        = sqrt(1106)
        ≒ 33.3

  GP_B (10R 6/8): RGB_B = (204, 126, 92)
    d_B = sqrt((198-204)² + (134-126)² + (120-92)²)
        = sqrt(36 + 64 + 784)
        = sqrt(884)
        ≒ 29.7

  GP_C (5YR 6/8): RGB_C = (200, 138, 75)
    d_C = sqrt((198-200)² + (134-138)² + (120-75)²)
        = sqrt(4 + 16 + 2025)
        = sqrt(2045)
        ≒ 45.2

重み(IDW, p=1):
  w_A = 1 / 33.3 ≒ 0.0300
  w_B = 1 / 29.7 ≒ 0.0337
  w_C = 1 / 45.2 ≒ 0.0221
  合計 W = 0.0858

明度の加重平均:
  V = (7 × 0.0300 + 6 × 0.0337 + 6 × 0.0221) / 0.0858
    = (0.210 + 0.202 + 0.133) / 0.0858
    ≒ 6.4

彩度の加重平均:
  C = (6 × 0.0300 + 8 × 0.0337 + 8 × 0.0221) / 0.0858
    = (0.180 + 0.270 + 0.177) / 0.0858
    ≒ 7.3

色相角のラップアラウンド処理

マンセルの色相は360度の循環構造を持つ。たとえば10RP(角度約351度)と2.5R(角度約0度)の平均を単純に計算すると(351 + 0) / 2 = 175.5度となり、本来近い赤系ではなく正反対の緑系に飛んでしまう。

これを防ぐために、色相角をcos/sinのベクトル成分に分解してから加重平均し、atan2で角度に復元する方法が使える。

色相角のベクトル平均:
  GP_A: hueAngle = 45°  → cos(45°) = 0.707, sin(45°) = 0.707
  GP_B: hueAngle = 27°  → cos(27°) = 0.891, sin(27°) = 0.454
  GP_C: hueAngle = 45°  → cos(45°) = 0.707, sin(45°) = 0.707

  加重cos = (0.707×0.0300 + 0.891×0.0337 + 0.707×0.0221) / 0.0858
           ≒ 0.767
  加重sin = (0.707×0.0300 + 0.454×0.0337 + 0.707×0.0221) / 0.0858
           ≒ 0.625
  復元角 = atan2(0.625, 0.767) ≒ 39.2°
  → 最寄り色相: 5YR(角度45°に最も近い標準色相)

この方式なら360度→0度の境界を跨ぐ場合でも正しい方向の平均が得られる。

ゼロ除算回避

入力RGBが格子点の代表値と完全に一致する場合、距離d = 0となり 1/d が発散する。実装ではソート後の最小距離を先にチェックし、d = 0ならその格子点のマンセル値をそのまま返して補間計算をスキップする。

if (top[0].dist === 0) {
  return {
    hue: top[0].gp.hue,
    value: top[0].gp.value,
    chroma: top[0].gp.chroma,
  };
}

epsilon(例: 1e-6)で下限クリップする方法もあるが、完全一致は格子点そのものの値を返すのが意味的に正しいため、明示的な分岐を採用した。

WCAG相対輝度計算との前処理共有

コントラスト比の算出にはsRGBのガンマ逆変換(リニア化)が必要になる。この前処理はIDWの距離計算とは独立しているが、同じ srgbToLinear 関数を共有することで実装の重複を避けている。

sRGBリニア化:
  s = channel / 255
  linear = (s <= 0.04045) ? s / 12.92 : ((s + 0.055) / 1.055) ^ 2.4

相対輝度:
  L = 0.2126 × linear_R + 0.7152 × linear_G + 0.0722 × linear_B

コントラスト比:
  ratio = (L_lighter + 0.05) / (L_darker + 0.05)

WCAGではこの比が4.5以上でAA基準、7以上でAAA基準を満たすと判定する。色変換ツールにコントラストチェック機能を併設する場合、リニア化関数の共有が設計上の合理的な判断となる。

検証結果

ケース1: 中間色(肌色系)の変換

建築の外壁塗装で頻出する肌色系の色を入力した場合。中間的な彩度であり、格子点が密な領域に該当する。

入力値:

  • RGB: (198, 134, 120)

計算結果:

  • 上位3格子点の距離: d_A ≒ 33.3, d_B ≒ 29.7, d_C ≒ 45.2
  • 補間色相角: 約39.2度 → 最寄り 5YR
  • 補間明度: 約6.4 → 丸め 6
  • 補間彩度: 約7.3 → 丸め 7
  • 最終出力: 5YR 6/7(近似)

解釈: 中間色領域では格子点間の距離が小さいため、IDWの近似誤差も小さく収まる。外壁のサンプル帳と照合しても実用上の差異は軽微な範囲。

ケース2: 高彩度色(鮮黄)の変換

sRGB色域の端に近い高彩度の黄色を入力した場合。格子点の最大彩度を超えるケース。

入力値:

  • RGB: (255, 220, 0)

計算結果:

  • 最寄り格子点群はすべて5Y系(角度81度付近)
  • 補間彩度: 約15.2 → 格子上の最大彩度6を大幅に超過
  • クランプ処理: 出力可能な最大彩度に丸め
  • 最終出力: 5Y 8/6(格子範囲内にクランプ)

解釈: 高彩度領域ではIDWの補間結果が格子の表現範囲を超えることがある。この場合は clampRGB 相当の処理で色域内に収める。ユーザーには「近似」である旨を免責表示で明示し、最終色は現物サンプルでの確認を促している。

エッジケース: 格子点完全一致

入力値:

  • RGB: (130, 130, 130) — 無彩色 N5 の格子点代表値と完全一致

計算結果:

  • 距離ソート後の先頭 d = 0 を検出
  • IDW補間をスキップし、格子点のマンセル値を直接返却
  • 最終出力: N5

解釈: 完全一致時にIDW計算を行うとゼロ除算が発生する。先頭チェックによる早期リターンで回避しており、無彩色の正確な表記が保証される。このパターンはN系(無彩色)の格子点9段階すべてで発生しうる。

よくある質問(FAQ)

Q: マンセル表記の精度はどの程度か

格子点が密な中間色・中彩度領域では、色相の誤差は1ステップ(9度)以内に収まることが多い。高彩度領域や格子点が疎な色域では誤差が大きくなるため、あくまで近似変換として扱い、厳密な色指定が必要な場面では現物のマンセル色票で確認してほしい。

Q: 色相角の加重平均でベクトル方式を使う理由は

色相は360度で循環するため、角度をそのまま算術平均すると境界(360度と0度の間)で誤った方向の平均が算出される。cos/sinに分解してベクトル和を取り、atan2で復元する方式ならラップアラウンドの影響を受けない。この手法は風向の平均計算などでも広く使われている。

Q: データはサーバーに送信される?

すべての計算はブラウザ内で完結する。入力した色の情報がサーバーに送信されることはない。履歴機能を有効にした場合のみlocalStorageに保存されるが、ブラウザ内に閉じた保存であり、外部からアクセスされることはない。

Q: 格子点データを増やせば精度は上がるか

原理的には格子点を増やすほど補間精度は向上する。ただし格子点数が増えると距離計算の走査コストもO(N)で増加するため、体感速度とのトレードオフになる。現状の約70点構成でもリアルタイム操作に支障はないが、数百点規模に拡張する場合はKD-tree等の空間索引の導入を検討する必要がある。

まとめ

RGB→マンセル変換において、IDW(逆距離加重補間)はブラウザ完結性・計算コスト・結果の説明性のバランスが最も妥当な方式だ。色相角のベクトル平均とゼロ除算回避の2点が実装上の要所であり、WCAG相対輝度計算とはsRGBリニア化関数を共有することで整合性を保っている。

対応アプリを試したいときはマンセル⇄HEXブリッジを使ってみて。コード体系の変換という共通テーマでは、鋼材断面のコンシェルジュも規格値と実務値の橋渡しを行っている。


不具合や要望があれば、お問い合わせページから気軽に教えてほしい。

M

Mahiro

Mahiro Appの開発者。逆距離加重補間(IDW)による色空間変換アルゴリズムを技術的に解説した記事の著者。

運営者情報を見る

© 2026 Mahiro App