= ディスプレイ表示時に輝度勾配が一定になるカラースケール
== はじめに
物理的な値の強弱の分布を見たい場合(例:サーモグラフィー)、値の強弱をカラースケールで色分けした画像で表現することが良くあると思います。
値が弱い順に青→緑→黄→橙→赤 と色相を一巡するようなものや、黒→赤→白、黒→青→白、黒→灰→白、と単色系のものがありますが、
今回は後者の単色系で、かつ、ディスプレイ表示時の輝度勾配が一定になるようなカラースケール画像を作成してみました。
xy値を指定すると、その系統の色でカラースケールが作られます。sRGB 色空間で計算しています。
== 計算方法
まず、xy色度値から、RGB化したときに色飛びしない限界の Y値(Y_nrmed)を求めます。
そして、Y値を 0~1にガンマ補正を掛けながら増加させて色を付けていくことでカラースケール化します。このとき、
* Y_nrmed より値が小さい範囲については、そのまま XYZ → RGB 変換を掛けます。
* Y_nrmed より値が大きい範囲については、そのまま XYZ → RGB 変換を掛けると RGB値が表示可能な範囲を超えてしまうため、色度を犠牲にして輝度を合わせに行く(段々白くする)処理を入れます。白(255,255,255)と、Y_nrmedのときのRGB値を内挿する処理になっています。
画像ファイル出力には、[https://libgd.github.io/ GD Graphics Library]を使ってみました。
== 結果
赤や緑で、処理が切り替わる付近で少し折れ目のようなものが見える気がします…
[[Embed(result.png)]]
== ソース
{{{
#include "gd.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <vector>
#include <map>
#include <algorithm>
// 参考にした URL
// http://fujiwaratko.sakura.ne.jp/infosci/colorspace/colorspace2.html
// https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB
// https://qiita.com/mmori3/items/edf4d65327c74f07de93
// https://matrixcalc.org/ja/#%7B%7B0%2e636958,0%2e144617,0%2e168881%7D,%7B0%2e2627,0%2e677998,0%2e0593017%7D,%7B0%2e0,0%2e0280727,1%2e06099%7D%7D%5E%28-1%29
// https://en.wikipedia.org/wiki/Rec._2020
// http://www.jislotz.net/docs/color/20130616.html
// https://www.rit.edu/cos/colorscience/rc_munsell_renotation.php
// https://www.color-sample.com/popular/munsell/
// [X] [R]
// [Y] = M x [G]
// [Z] [B]
//
// <sRGB>
// [0.412391 0.357584 0.180481]
// M = [0.212639 0.715169 0.072192]
// [0.019331 0.119195 0.950532]
//
// [ 3.240970 -1.537383 -0.498611]
// M' = [-0.969244 1.875968 0.041555]
// [ 0.055630 -0.203977 1.056972]
// <BT.2020>
// [0.636958 0.144617 0.168881]
// M = [0.2627 0.677998 0.0593017]
// [0.0 0.0280727 1.06099]
//
// [ 1.716651 -0.3556711 -0.2533652 ]
// M' = [-0.6666839 1.616481 0.01576844]
// [ 0.01763977 -0.04277043 0.9420987 ]
namespace sRGB{
double RGB_to_Y(double r, double g, double b){
return 0.212639 * r + 0.715169 * g + 0.072192 * b;
}
void XYZ_to_RGB(double X, double Y, double Z, double rgb[3]){
rgb[0] = 3.240970 * X + -1.537383 * Y + -0.498611 * Z;
rgb[1] = -0.969244 * X + 1.875968 * Y + 0.041555 * Z;
rgb[2] = 0.055630 * X + -0.203977 * Y + 1.056972 * Z;
}
#if 1
double Inverse_Gamma(double y){
if (y < 0.0031308) return 12.92 * y;
return 1.055 * std::pow(y, 1.0 / 2.4) - 0.055;
}
#else
double Inverse_Gamma(double y){
return std::pow(y, 1.0 / 2.2);
}
#endif
}
namespace BT2020{
double RGB_to_Y(double r, double g, double b){
return 0.2627 * r + 0.677998 * g + 0.0593017 * b;
}
void XYZ_to_RGB(double X, double Y, double Z, double rgb[3]){
rgb[0] = 1.716651 * X + -0.3556711 * Y + -0.2533652 * Z;
rgb[1] = -0.6666839 * X + 1.616481 * Y + 0.01576844 * Z;
rgb[2] = 0.01763977 * X + -0.04277043 * Y + 0.9420987 * Z;
}
double Inverse_Gamma(double y){
const static double a = 1.09929682680944, b = 0.018053968510807;
if (y < b) return 4.5 * y;
return a * std::pow(y, 0.45) - (a - 1.0);
}
}
void Yxy_to_XYZ(double Y, double x, double y, double &X, double &Z){
X = x * Y / y;
Z = (1 - x - y) * Y / y;
}
int main(int argc, char *argv[]){
double xy_targets[][2] ={
{0.64 , 0.33}, // sRGB 赤
{0.30 , 0.60}, // sRGB 緑
{0.2545 0.3855}, // マンセル値 7.5Gv9c10(青み掛かった緑) の xy値
{0.15 , 0.06}, // sRGB 青
{0.3127, 0.3290}, // 白(D65)
{-100, -100} // 番兵
};
int xy_cnt = 0;
for (int i = 0; xy_targets[i][0] > -100; ++i) xy_cnt++;
gdImagePtr img = gdImageCreateTrueColor(512, 32*xy_cnt);
for (int k = 0; k < xy_cnt; ++k){
double *const xy_target = xy_targets[k];
std::printf("xy: %f %f\n", xy_target[0], xy_target[1]);
double rgb_nrm[3] = {0}; // rgb各要素の最大値を1に正規化した値
double Y_nrmed; // rgb に対応するY値
double Y_nrmed_invg;
{
double X, Z;
Yxy_to_XYZ(1.0, xy_target[0], xy_target[1], X, Z);
sRGB::XYZ_to_RGB(X, 1.0, Z, rgb_nrm);
ptrdiff_t max_idx = std::max_element(rgb_nrm, rgb_nrm+3) - rgb_nrm;
Y_nrmed = 1.0 / rgb_nrm[max_idx];
Yxy_to_XYZ(Y_nrmed, xy_target[0], xy_target[1], X, Z);
sRGB::XYZ_to_RGB(X, Y_nrmed, Z, rgb_nrm);
// この時点で、 RGB 各要素の最大値が 1 に正規化された RGB 値が得られているはず
}
std::printf("RGB_nrm: %f %f %f, Y_nrmed:%f\n", rgb_nrm[0], rgb_nrm[1], rgb_nrm[2], Y_nrmed);
{
using namespace sRGB;
for (int j = 32 * k; j < 32 * (k + 1); ++j){
for (int i = 0; i < 512; ++i){
double Y_i = Inverse_Gamma(static_cast<double>(i) / 512.0);
double rgb[3];
if (Y_i <= Y_nrmed){
double X, Z;
Yxy_to_XYZ(Y_i, xy_target[0], xy_target[1], X, Z);
XYZ_to_RGB(X, Y_i, Z, rgb);
}else{
double t = (Y_i - Y_nrmed) / (1.0 - Y_nrmed);
for (int i = 0; i < 3; ++i){
rgb[i] = rgb_nrm[i] * (1.0 - t) + t;
}
}
auto rgb8bit = [&](double rgb_){
double inv_g = rgb_;
if (inv_g < 0) return 0;
else if (inv_g > 1) return 255;
else return static_cast<int>(inv_g * 255.0);
};
img->tpixels[j][i] = gdTrueColor(rgb8bit(rgb[0]), rgb8bit(rgb[1]), rgb8bit(rgb[2]));
}
}
}
}
gdImageFile(img, "colorbar.png");
#if 0
// gd のグレースケール変換は BT.601 のため、 sRGB とは変換式が異なる。
gdImageGrayScale(img);
#else
// sRGB でグレースケール化
for (int j = 0; j < gdImageSY(img); ++j){
for (int i = 0; i < gdImageSX(img); ++i){
int &rgb = img->tpixels[j][i];
int gray8 = sRGB::RGB_to_Y(gdImageRed(img, rgb), gdImageGreen(img, rgb), gdImageBlue(img, rgb));
img->tpixels[j][i] = gdTrueColor(gray8, gray8, gray8);
}
}
#endif
gdImageFile(img, "colorbar_grayscaled.png");
gdImageDestroy(img);
return 0;
}
}}}
== 参考サイト
* [http://fujiwaratko.sakura.ne.jp/infosci/colorspace/colorspace2.html]
* [https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB]
* [https://qiita.com/mmori3/items/edf4d65327c74f07de93]
* [https://matrixcalc.org/ja/#%7B%7B0%2e636958,0%2e144617,0%2e168881%7D,%7B0%2e2627,0%2e677998,0%2e0593017%7D,%7B0%2e0,0%2e0280727,1%2e06099%7D%7D%5E%28-1%29]
* [https://en.wikipedia.org/wiki/Rec._2020]
* [http://www.jislotz.net/docs/color/20130616.html]
* [https://www.rit.edu/cos/colorscience/rc_munsell_renotation.php]
* [https://www.color-sample.com/popular/munsell/]