Webサービス・アプリを色から検索できる機能をリリースした話(色差・近似色)


はじめに

最近、話題のWebサービス・アプリが見つかる「 Apppla(アプラ) 」を開発・運営しています。その中で、サービスをカテゴリや紹介文といったテキストからの一般的な検索だけでなく、サービスカラーから検索する機能を実装した話をします。
コードの話というよりは考え方やアルゴリズム?の話です。

  • Appplaとは?
  • 色差アルゴリズム
  • 色検索のアプローチ

Appplaとは?

話題のWebサービス・アプリが見つかるサイトです。Techable, THE BRIDGE, Tech Crunch, PR TIMES, ProductHuntなど約50のメディアから最新のWebサービス・アプリ情報をApppla公認のキュレーターによって毎日お届けします。

見ていただけると嬉しいです\(^o^)/
URL: https://www.appp.la

色からWebサービス・アプリを探す機能は以下から確認できます。
https://www.appp.la/colors

色差計算アルゴリズム

2つの色の知覚的な差を定量化するアルゴリズムはいくつかあり、いくつかの手法を紹介します。

  • RGB
  • CIE76
  • CIE94

RGB

色は16進数(#ffffff)で表記できますが、これはR(Red), G(Green), B(Blue)の3つの要素で構成されており、それらの要素に対してユークリッド距離を求めて、[この距離が小さい = 色が近い]とする方法。

色1(R1, G1, B1)と色2(R2, G2, B2)の差は


(「
Color difference」『ウィキペディア』より引用)

CIE76

前述のRGBは馴染み深いと思うのですが、この方法ではLabというものが使われています。

Lab色空間は明度を意味する次元 L と補色次元の a および b を持ち、CIE XYZ 色空間の座標を非線形に圧縮したものに基づいた補色空間です。RGB出力機器の都合が優先されていますが、Lab色空間は人間の視覚を近似するよう設計されているのが特徴です。

一般的なRGBからLab色空間に変換を行う必要がありますが、その変換作業は下で説明するOSS(Ruby gems, .NET Library)があるので割愛します。

変換を行った後は、色1(L1, a1, b1)と色2(L2, a2, b2)の差は


(「
Color difference」『ウィキペディア』より引用)

で求められます。この値が小さい色が似ている色ということになります。

CIE94

CIE94は、塗料やコーディング業界で利用されている色の表現方法です。Lab色空間を維持しながら、知覚的な不均一に対応するように拡張されている。

色1(L1, a1, b1)と色2(L2, a2, b2)の差は


(「
Color difference」『ウィキペディア』より引用)

で求められます。この数式で使用している数式の説明は上記のWikiより参照してください。


色差の計算方法は他にもいろいろあってRGB to Labなどは既にOSSなどが公開されているので、そういったものを利用することをおすすめします。

色検索のアプローチ

今回の実装では、RGBでの色差計算を採用しました。色をRGB形式で保存していたこともあり、楽そうだったというだけの理由です。
ただ、毎回上記のアルゴリズムで色の近似を計算していては一般的なデータベースは色検索には特化していないし、最適化も難しいので、色検索のフローを簡素化することで解決しました(多分これが一般的な解決方法な気がします)。

あらかじめ、基本となる色を決めます。Appplaでは上記の20色を選定しています(この色の選定はマテリアルカラー表を参考にしたのですが今になってミスったなと思っています)。
近似色を保存するカラムをあらかじめ作成しておき、データベースに色を登録する際に、上記のアルゴリズムで基本色の中で一番近い色を近似色カラムに保存します。そして、あるrgbに近い色を検索する時は、rgbと基本色20色から近い色を近似色に持つモデルを引っ張って来れば完了です。

実装した色検索のフロー

  1. データベースにrgb値を保存する際に、あらかじめ決めた基本色に近い色を近似色として保存します。
  2. 検索したい色(rgb)を受け取る
  3. 受け取ったrgbと近い基本色を求める
  4. 3で求めた基本色を近似色とするモデルを取得

色情報の取得は、Appplaで紹介されたサービス全体のキャプチャー画像を撮り、RubyのMiniMagickを使用してその画像から支配色を抽出してカラーコードを生成しています。

def take_colors(image_url, quantity: 10, threshold: 1)
  return [] if image_url.nil?
  image = MiniMagick::Image.open(image_url)
  result = image.run_command('convert', image_url, '-format', '%c', '-colors', quantity, '-depth', 8, 'histogram:info')

  # Extract colors and frequencies from result
  frequencies = result.scan(/([0-9]+)\:/).flatten.map { |m| m.to_f }
  hex_values = result.scan(/(\#[0-9ABCDEF]{6})/).flatten

  total_frequencies = colors.reduce(0.0) { |sum, (_, freq)| sum += freq }.to_f
  colors.
    map { |(hex, freq)| [hex, (freq * 100 / total_frequencies).to_i] }.
    reject { |(hex, freq)| freq < threshold }.
    sort_by { |(hex, freq)| freq }.reverse
end

参考サイト