「彩度調整 - 酢ろぐ!」では、彩度を求める計算を簡略化していたのですが、NuGet Pakageを作るついでにチェックしていると計算ロジックが微妙かも……と思ってしまったので、ピクセルデータを一旦HSVへ変換した上で彩度を調整し、RGBに戻すように調整しました。
RGBからHSVへの変換、HSVからRGBへの変換方法が書かれているサイトは結構あるのですが、一番分かりやすく変換方法が書かれていたのがWikipediaのHSV色空間でした。この記事を元にRGB to HSV、HSV to RGBを実装してみました。
今日書いたコードは、GitHubにアップしています。⇒ HSV.cs
**RGB to HSV
byte型のRGB値からHSVへ変換します。それぞれHue(色相)、Saturation(彩度)、Value(明度)を保持するプロパティを作成します。
using System; namespace Softbuild.Media.Effects { /// <summary> /// ピクセルデータをHSV色空間で表したクラス /// </summary> public class HSV { /// <summary> /// HSV クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="h">色相</param> /// <param name="s">彩度</param> /// <param name="v">明度</param> public HSV(double h, double s, double v) { Hue = h; Saturation = s; Value = v; } /// <summary> /// HSV クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="hsv">HSV各要素の値が格納されたdouble配列</param> public HSV(double[] hsv) { Hue = hsv[0]; Saturation = hsv[1]; Value = hsv[2]; } /// <summary> /// 色相 /// </summary> public double Hue { get; set; } /// <summary> /// 彩度 /// </summary> public double Saturation { get; set; } /// <summary> /// 明度 /// </summary> public double Value { get; set; }
HSVクラスのstaticとしてFromRGBメソッドを作成します。
/// <summary> /// RGB値を元に HSV クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="r">R要素の値</param> /// <param name="g">G要素の値</param> /// <param name="b">B要素の値</param> /// <returns>HSVオブジェクト</returns> public static HSV FromRGB(double r, double g, double b) { // R、GおよびBが0.0を最小量、1.0を最大値とする0.0から1.0の範囲にある r /= 255; g /= 255; b /= 255; var max = Math.Max(Math.Max(r, g), b); var min = Math.Min(Math.Min(r, g), b); var sub = max - min; double h = 0, s = 0, v = 0; // Calculate Hue if (sub == 0) { // MAX = MIN(例・S = 0)のとき、 Hは定義されない。 h = 0; } else { if (max == r) { h = (60 * (g - b) / sub) + 0; } else if (max == g) { h = (60 * (b - r) / sub) + 120; } else if (max == b) { h = (60 * (r - g) / sub) + 240; } // さらに H += 360 if H < 0 if (h < 0) { h += 360; } } // Calculate Saturation if (max > 0) { s = sub / max; } // Calculate Value v = max; return new HSV(h, s, v); }
**HSV to RGB
計算結果を格納するRGBクラスを用意します。
using System; namespace Softbuild.Media.Effects { /// <summary> /// ピクセルデータをRGB色空間で表したクラス /// </summary> public class RGB { /// <summary> /// RGB クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="r">赤成分</param> /// <param name="g">緑成分</param> /// <param name="b">青成分</param> public RGB(byte r, byte g, byte b) { Red = r; Green = g; Blue = b; } /// <summary> /// RGB クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="r">赤成分</param> /// <param name="g">緑成分</param> /// <param name="b">青成分</param> public RGB(double r, double g, double b) { Red = (byte)r; Green = (byte)g; Blue = (byte)b; } /// <summary> /// 赤成分 /// </summary> public byte Red { get; set; } /// <summary> /// 緑成分 /// </summary> public byte Green { get; set; } /// <summary> /// 青成分 /// </summary> public byte Blue { get; set; } } }
HSVクラスにToRGBメソッドを追加します。
/// <summary> /// RGBへ変換する /// </summary> /// <returns>RGBオブジェクト</returns> public RGB ToRGB() { // まず、もしSが0.0と等しいなら、最終的な色は無色もしくは灰色である。 if (Saturation == 0) { return new RGB(Value * 255, Value * 255, Value * 255); } double r = 0, g = 0, b = 0; double f = 0; double p = 0, q = 0, t = 0; //var h = Math.Min(360.0, Math.Max(0, Hue)); // 角座標系で、Hの範囲は0から360までであるが、その範囲を超えるHは360.0で // 割った剰余(またはモジュラ演算)でこの範囲に対応させることができる。 // たとえば-30は330と等しく、480は120と等しくなる。 var h = Hue % 360; var s = Math.Min(1.0, Math.Max(0, Saturation)); var v = Math.Min(1.0, Math.Max(0, Value)); var hi = (int)(h / 60); f = (h / 60) - hi; p = v * (1 - s); q = v * (1 - f * s); t = v * (1 - (1 - f) * s); if (hi == 0) { r = v; g = t; b = p; } else if (hi == 1) { r = q; g = v; b = p; } else if (hi == 2) { r = p; g = v; b = t; } else if (hi == 3) { r = p; g = q; b = v; } else if (hi == 4) { r = t; g = p; b = v; } else if (hi == 5) { r = v; g = p; b = q; } return new RGB(r * 255, g * 255, b * 255); }
**SaturationEffect.cs
RGBクラスとHSVクラスを導入したので、SaturationEffectクラスの実装も変更してみます。
/// <summary> /// 彩度調整処理をおこなう /// </summary> /// <param name="width">ビットマップの幅</param> /// <param name="height">ビットマップの高さ</param> /// <param name="source">処理前のピクセルデータ</param> /// <returns>処理後のピクセルデータ</returns> public byte[] Effect(int width, int height, byte[] source) { int pixelCount = width * height; var dest = new byte[source.Length]; for (int i = 0; i < pixelCount; i++) { var index = i * 4; // 処理前のピクセルの各ARGB要素を取得する double b = source[index + 0]; double g = source[index + 1]; double r = source[index + 2]; double a = source[index + 3]; // 色空間をRGBからHSVへ変換する var hsv = HSV.FromRGB(r, g, b); // 彩度の調整をおこなう hsv.Saturation *= Saturation; // 色空間をHSVからRGBへ変換する var rgb = hsv.ToRGB(); int db = rgb.Blue; int dg = rgb.Green; int dr = rgb.Red; // 処理後のバッファへピクセル情報を保存する dest[index + 0] = (byte)Math.Min(255, Math.Max(0, db)); dest[index + 1] = (byte)Math.Min(255, Math.Max(0, dg)); dest[index + 2] = (byte)Math.Min(255, Math.Max(0, dr)); dest[index + 3] = source[index + 3]; } return dest; }