酢ろぐ!

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

彩度調整その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(明度)を保持するプロパティを作成します。

|cs| using System;

namespace Softbuild.Media.Effects { ///

/// ピクセルデータをHSV色空間で表したクラス /// public class HSV { /// /// HSV クラスの新しいインスタンスを初期化します。 /// /// 色相 /// 彩度 /// 明度 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メソッドを作成します。

|cs| ///

/// RGB値を元に HSV クラスの新しいインスタンスを初期化します。 /// /// R要素の値 /// G要素の値 /// B要素の値 /// HSVオブジェクト 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クラスを用意します。

|cs| using System;

namespace Softbuild.Media.Effects { ///

/// ピクセルデータをRGB色空間で表したクラス /// public class RGB { /// /// RGB クラスの新しいインスタンスを初期化します。 /// /// 赤成分 /// 緑成分 /// 青成分 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メソッドを追加します。

|cs| ///

/// RGBへ変換する /// /// RGBオブジェクト 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クラスの実装も変更してみます。

|cs| ///

/// 彩度調整処理をおこなう /// /// ビットマップの幅 /// ビットマップの高さ /// 処理前のピクセルデータ /// 処理後のピクセルデータ 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;
    }

||<