agehama's diary

年一更新

RGBとRYB、色相環の話

f:id:agehama:20190304214651p:plain
色空間ごとの色相環(左:RGB(HSV)、中:RYB(文献1)、右:RYB(文献2))


色彩理論では色相環上での反対位置(補色)や正三角形の位置にある色同士は相性がいいらしい。→配色の調和

ただし、ここでいう色相環とはマンセル表色系など人間の知覚に基づいた色空間上のものであり、ただ HSV をずらっと並べてもガタガタな輪っかしかできない。

例:
qiita.com
design-spice.com

とりあえず RGB より良さそうで実装が簡単そうな RYB(Red-Yellow-Blue) 表色系について調べた。

(1) RYB Color Compositing

文献1は文献2が RYB → RGB の非可逆変換であることを指摘し、相互変換可能な手法を提案している。
誤差がほぼゼロで変換も簡単なので汎用性は高そう。
この手法で作った色相環がトップ画像中央のもので、RGB と比べるとオレンジの幅が広くなったのと紫が薄くなった印象があるけどあんまり滑らかな感じはしない…

(2) Paint Inspired Color Mixing and Compositing for Visualization

文献2は RYB 三次元空間上の8つの格子点それぞれに対応する RGB 値を置き、その間を線形補間することで RYB → RGB への変換を行うもの。
この手法の結果がトップ画像右のもので、左二つより大分いい感じがする。
RYB への逆変換を行う方法についての議論は見つかったが未解決なようなので、実用するとしたら近似するしかないのかな。

参考文献

(1) RYB Color Compositing
(2) Paint Inspired Color Mixing and Compositing for Visualization誤植修正版
(3) transformation - RYB and RGB color space conversion - Mathematics Stack Exchange
(4) 配色の調和 | 基礎編 | TOYO INK 1050+

以下ソースコード


Main.cpp

# include <Siv3D.hpp> // OpenSiv3D v0.3.0

//RYB Color Compositing
//http://nishitalab.org/user/UEI/publication/Sugita_IWAIT2015.pdf
struct RYB1
{
	RYB1() = default;
	RYB1(double r, double y, double b) :r(r), y(y), b(b) {}

	Color rgb()const
	{
		const double i_w = Min({ r, y, b });
		const double r_ryb = r - i_w;
		const double y_ryb = y - i_w;
		const double b_ryb = b - i_w;
		const double r_rgb = r_ryb + y_ryb - Min(y_ryb, b_ryb);
		const double g_rgb = y_ryb + 2.0 * Min(y_ryb, b_ryb);
		const double b_rgb = 2.0 * (b_ryb - Min(y_ryb, b_ryb));
		const double n = Max({ r_rgb, g_rgb, b_rgb }) / Max({ r_ryb, y_ryb, b_ryb });
		const double i_b = Min({ 1 - r, 1 - y, 1 - b });
		return ColorF(r_rgb / n + i_b, g_rgb / n + i_b, b_rgb / n + i_b);
	}

	RYB1 lerp(const RYB1& other, double t)const
	{
		return RYB1(Math::Lerp(r, other.r, t), Math::Lerp(y, other.y, t), Math::Lerp(b, other.b, t));
	}

	double r, y, b;// in [0.0, 1.0]
};

//Paint Inspired Color Mixing and Compositing for Visualization
//http://web.siat.ac.cn/~baoquan/papers/InfoVis_Paint.pdf
struct RYB2
{
	RYB2() = default;
	RYB2(double r, double y, double b) :r(r), y(y), b(b) {}

	Color rgb()const
	{
		const Vec3 p000(1, 1, 1);
		const Vec3 p001(0.163, 0.373, 0.6);
		const Vec3 p010(1, 1, 0);
		const Vec3 p011(0, 0.66, 0.2);
		const Vec3 p100(1, 0, 0);
		const Vec3 p101(0.5, 0, 0.5);
		const Vec3 p110(1, 0.5, 0);
		const Vec3 p111(0.2, 0.094, 0);
		return ColorF(p000 * (1 - r)*(1 - y)*(1 - b) + p001 * (1 - r)*(1 - y)*b
			+ p010 * (1 - r)*y*(1 - b) + p100 * r*(1 - y)*(1 - b)
			+ p011 * (1 - r)*y*b + p101 * r*(1 - y)*b
			+ p110 * r*y*(1 - b) + p111 * r*y*b);
	}

	RYB2 lerp(const RYB2& other, double t)const
	{
		return RYB2(Math::Lerp(r, other.r, t), Math::Lerp(y, other.y, t), Math::Lerp(b, other.b, t));
	}

	double r, y, b;// in [0.0, 1.0]
};

void Main()
{
	Graphics::SetBackground(Palette::White);

	while (System::Update())
	{
		const double innerRadius = 60;
		const double outerRadius = 100;
		const double margin = 20;

		{
			const Vec2 center = Vec2(Window::Width() / 2 - outerRadius * 2 - margin, Window::Height() / 2);
			
			int divNum = 100;
			const double unitAngle = 360_deg / divNum;
			for (int i = 0; i < divNum; ++i)
			{
				const double beginAngle = unitAngle * i;
				const double endAngle = unitAngle * (i + 1);

				const Color beginColor = HSV(ToDegrees(beginAngle), 1, 1);
				const Color endColor = HSV(ToDegrees(endAngle), 1, 1);

				const Color beginColorGray = ColorF(beginColor.grayscale());
				const Color endColorGray = ColorF(endColor.grayscale());

				const Vec2 p1 = Vec2(cos(beginAngle), sin(beginAngle))*outerRadius;
				const Vec2 p2 = Vec2(cos(endAngle), sin(endAngle))*outerRadius;
				const Vec2 p3 = Vec2(cos(endAngle), sin(endAngle))*innerRadius;
				const Vec2 p4 = Vec2(cos(beginAngle), sin(beginAngle))*innerRadius;

				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, -110)).draw(beginColor, endColor, endColor, beginColor);
				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, +110)).draw(beginColorGray, endColorGray, endColorGray, beginColorGray);
			}
		}

		{
			const RYB1 yellow(0, 1, 0), red(1, 0, 0), blue(0, 0, 1);
			const RYB1 orange = yellow.lerp(red, 0.5);
			const RYB1 violet = red.lerp(blue, 0.5);
			const RYB1 green = blue.lerp(yellow, 0.5);

			const RYB1 yellow_orange = yellow.lerp(orange, 0.5);
			const RYB1 orange_red = orange.lerp(red, 0.5);
			const RYB1 red_violet = red.lerp(violet, 0.5);
			const RYB1 violet_blue = violet.lerp(blue, 0.5);
			const RYB1 blue_green = blue.lerp(green, 0.5);
			const RYB1 green_yellow = green.lerp(yellow, 0.5);

			const std::vector<RYB1> colors({
				red,orange_red,orange,yellow_orange,yellow,green_yellow,
				green,blue_green,blue,violet_blue,violet,red_violet
				});

			const Vec2 center = Vec2(Window::Width() / 2, Window::Height() / 2);
			
			int divNum = 100;
			const double unitAngle = 360_deg / divNum;
			for (int i = 0; i < divNum; ++i)
			{
				const double beginAngle = unitAngle * i;
				const double endAngle = unitAngle * (i + 1);

				const double beginT = fmod(ToDegrees(beginAngle), 30.0) / 30.0;
				const double endT = fmod(ToDegrees(endAngle), 30.0) / 30.0;

				const int beginIndex_lower = static_cast<int>(ToDegrees(beginAngle) / 30.0) % 12;
				const int beginIndex_upper = (beginIndex_lower + 1) % 12;
				const int endIndex_lower = static_cast<int>(ToDegrees(endAngle) / 30.0) % 12;
				const int endIndex_upper = (endIndex_lower + 1) % 12;
				
				const Color beginColor = colors[beginIndex_lower].lerp(colors[beginIndex_upper], beginT).rgb();
				const Color endColor = colors[endIndex_lower].lerp(colors[endIndex_upper], endT).rgb();

				const Color beginColorGray = ColorF(beginColor.grayscale());
				const Color endColorGray = ColorF(endColor.grayscale());

				const Vec2 p1 = Vec2(cos(beginAngle), sin(beginAngle))*outerRadius;
				const Vec2 p2 = Vec2(cos(endAngle), sin(endAngle))*outerRadius;
				const Vec2 p3 = Vec2(cos(endAngle), sin(endAngle))*innerRadius;
				const Vec2 p4 = Vec2(cos(beginAngle), sin(beginAngle))*innerRadius;

				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, -110)).draw(beginColor, endColor, endColor, beginColor);
				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, +110)).draw(beginColorGray, endColorGray, endColorGray, beginColorGray);
			}
		}
		{
			const RYB2 yellow(0, 1, 0), red(1, 0, 0), blue(0, 0, 1);
			const RYB2 orange = yellow.lerp(red, 0.5);
			const RYB2 violet = red.lerp(blue, 0.5);
			const RYB2 green = blue.lerp(yellow, 0.5);

			const RYB2 yellow_orange = yellow.lerp(orange, 0.5);
			const RYB2 orange_red = orange.lerp(red, 0.5);
			const RYB2 red_violet = red.lerp(violet, 0.5);
			const RYB2 violet_blue = violet.lerp(blue, 0.5);
			const RYB2 blue_green = blue.lerp(green, 0.5);
			const RYB2 green_yellow = green.lerp(yellow, 0.5);

			const std::vector<RYB2> colors({
				red,orange_red,orange,yellow_orange,yellow,green_yellow,
				green,blue_green,blue,violet_blue,violet,red_violet
				});

			const Vec2 center = Vec2(Window::Width() / 2 + outerRadius * 2 + margin, Window::Height() / 2);
			int divNum = 100;
			const double unitAngle = 360_deg / divNum;
			for (int i = 0; i < divNum; ++i)
			{
				const double beginAngle = unitAngle * i;
				const double endAngle = unitAngle * (i + 1);

				const double beginT = fmod(ToDegrees(beginAngle), 30.0) / 30.0;
				const double endT = fmod(ToDegrees(endAngle), 30.0) / 30.0;

				const int beginIndex_lower = static_cast<int>(ToDegrees(beginAngle) / 30.0) % 12;
				const int beginIndex_upper = (beginIndex_lower + 1) % 12;
				const int endIndex_lower = static_cast<int>(ToDegrees(endAngle) / 30.0) % 12;
				const int endIndex_upper = (endIndex_lower + 1) % 12;

				const Color beginColor = colors[beginIndex_lower].lerp(colors[beginIndex_upper], beginT).rgb();
				const Color endColor = colors[endIndex_lower].lerp(colors[endIndex_upper], endT).rgb();

				const Color beginColorGray = ColorF(beginColor.grayscale());
				const Color endColorGray = ColorF(endColor.grayscale());

				const Vec2 p1 = Vec2(cos(beginAngle), sin(beginAngle))*outerRadius;
				const Vec2 p2 = Vec2(cos(endAngle), sin(endAngle))*outerRadius;
				const Vec2 p3 = Vec2(cos(endAngle), sin(endAngle))*innerRadius;
				const Vec2 p4 = Vec2(cos(beginAngle), sin(beginAngle))*innerRadius;

				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, -110)).draw(beginColor, endColor, endColor, beginColor);
				Quad(p1, p2, p3, p4).moveBy(center + Vec2(0, +110)).draw(beginColorGray, endColorGray, endColorGray, beginColorGray);
			}
		}
	}
}