酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

彩度調整その2 〜C#でRGBからHSVへの変換、HSVからRGBへの変換〜

彩度調整 - 酢ろぐ!」では、彩度を求める計算を簡略化していたのですが、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;
        }