C#

ColorMatrixによる色の変換

ビットマップの色の変換は、みんな自分なりのやり方があると思う。昔ながらにピクセルをひとつずつ調べて変換していくのも良いと思うが、C#では Graphics.DrawImage() を使うのが順当だろうか。何度かやっているが、どうも私はその度に引っ掛かっているようなので、メモしておくことにした。

ColorMatrix は 5×5 の正方行列で、色の成分を表す 1×5 の行列を変換するのに使われる。ヘルプでは 1×5 の行列を「アルファ、赤、緑、青、およびWで表現されるARGBベクタなど」って書いてあるので [A R G B W] だと思ってやると、訳の分からない結果になる。いつもここで引っ掛かる。実際は [R G B A W] だ。ウェブのヘルプでは「赤、緑、青、アルファおよびW」って書いてあった。

            ┌ m00 m01 m02 m03 m04 ┐
            │ m10 m11 m12 m13 m14 │
[R G B A W] │ m20 m21 m22 m23 m24 │= [R' G' B' A' W']
            │ m30 m31 m32 m33 m34 │
            └ m40 m41 m42 m43 m44 ┘

R' = R × m00 + G × m10 + B × m20 + A × m30 + W × m40
G' = R × m01 + G × m11 + B × m21 + A × m31 + W × m41
B' = R × m02 + G × m12 + B × m22 + A × m32 + W × m42
A' = R × m03 + G × m13 + B × m23 + A × m33 + W × m43
W' = R × m04 + G × m14 + B × m24 + A × m34 + W × m44

※ Wは1。W'は不要なので計算されないと思う。

[R G B A W] の各成分は [0:1] の範囲と想定されているようだ。演算結果の [R' G' B' A' W'] の各成分が [0:1] の範囲に収まるように ColorMatrix を設定したほうが良い。ただし、必ずしも [0:1] の範囲に収まらなくても大丈夫なようで、0未満の場合は0として、1を超える場合は1として扱われるらしい。

例1)20%明るくする。

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.8f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.8f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.8f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.2f, 0.2f, 0.2f, 0.0f, 1.0f }
    }
);

// 各色を80%にして、0.2底上げする。
// コンストラクタの引数はジャグ配列になっているので、注意しよう。
20%明るくする
例2)青味を強くする(その1)

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.7f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.3f, 0.0f, 1.0f }
    }
);
// 例1の応用。青だけを底上げして青味を強調する。
// 暗い部分が青っぽくなる。全体的に少し明るくなる。
青味を強くする(その1)
例3)青味を強くする(その2)

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.9f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.9f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
    }
);
// 赤と緑を減らすことで青を強調する。
// 青を含む明るい部分が青っぽくなる。全体的に少し暗くなる。
青味を強くする(その2)
例4)グレイスケールに変換する

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.2126f, 0.2126f, 0.2126f, 0.0f, 0.0f },
        new float[] { 0.7152f, 0.7152f, 0.7152f, 0.0f, 0.0f },
        new float[] { 0.0722f, 0.0722f, 0.0722f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
    }
);

// 重みづけの数値(R:0.2126, G:0.7152, B:0.0722)は、
// 難しいことはよくわからないし、青が小さすぎるような気もするが、
// Wiki に載っていたのを無暗に信用して採用した。
グレイスケールに変換する
例5)セピア調にする

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.5f * 0.1216f, 0.6f * 0.2126f, 0.7f * 0.2126f, 0.0f, 0.0f },
        new float[] { 0.5f * 0.7152f, 0.6f * 0.7152f, 0.7f * 0.7152f, 0.0f, 0.0f },
        new float[] { 0.5f * 0.0722f, 0.6f * 0.0722f, 0.7f * 0.0722f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.4f, 0.3f, 0.2f, 0.0f, 1.0f }
    }
);
// 例4の応用。
// 彩度(?)を下げて赤味を強調する。
// 想像では赤っぽいような黄色っぽいような感じだ。
セピア調にする
例6)半透明(50%)にする

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.5f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
    }
);

// アルファ値は不透過率を表しているようだ。
// 0にすると透明になり背景が表示される。
// 1にすると不透明になり画像が表示される。
// 0.3にすると、30%不透過=70%透過になる。そんな感じ。
半透明(50%)にする
例7)ネガにする

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { -1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, -1.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, -1.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f }
    }
);

// 多分、ネガを作ることは何の役にも立たないが、
// ネガフィルムをデータ化して作った画像から
// ポジを作るときには役に立つのかも。
ネガにする
例8)色相を半転する

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.0f, 0.5f, 0.5f, 0.0f, 0.0f },
        new float[] { 0.5f, 0.0f, 0.5f, 0.0f, 0.0f },
        new float[] { 0.5f, 0.5f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
    }
);

// 色相を表す円を想定し、円周を3等分する3点にR, G, Bを配置する。
// その円周上の任意の点を、RからGの方向へ半回転させ色相を変換する。
// 何かの役に立つとは思えない。
色相を半転する
例9)色相を入れ替える

float Value = 0.2f; // 任意の回転数を指定

const float THIRD = 1.0f / 3.0f;
const float TTHRD = 2.0f / 3.0f;

if (Value < 0.0f)
{
    Value -= (int)(Value - 1);
}
else if (Value >= 1.0f)
{
    Value -= (int)Value;
}

float Rate1 = 0.0f;
float Rate2 = 0.0f;
float Rate3 = 0.0f;

switch ((int)(Value / THIRD))
{
    case 0:
        Rate3 = Value / THIRD;
        Rate1 = 1.0f - Rate3;
        break;

    case 1:
        Rate2 = (Value - THIRD) / THIRD;
        Rate3 = 1.0f - Rate2;
        break;

    default: //case 2:
        Rate1 = (Value - TTHRD) / THIRD;
        Rate2 = 1.0f - Rate1;
        break;
}

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { Rate1, Rate3, Rate2, 0.0f, 0.0f },
        new float[] { Rate2, Rate1, Rate3, 0.0f, 0.0f },
        new float[] { Rate3, Rate2, Rate1, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
    }
);

// 例8を拡張したもの。任意の回転数で色相を変換する。
// 例8はValue=0.5の場合。
色相を入れ替える
例10)明るさを入れ替える

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.0f, -0.5f, -0.5f, 0.0f, 0.0f },
        new float[] { -0.5f, 0.0f, -0.5f, 0.0f, 0.0f },
        new float[] { -0.5f, -0.5f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
        new float[] { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f }
    }
);

// ネガにして色相を半回転させると、結果的に明るさが入れ替わる。
// 明るい青は暗い青になり、暗い青は明るい青になる。
// 白は黒になり、黒は白になる。灰色は変わらない。
明るさを入れ替える

他にも、グレイスケールにしてネガにするなどしたいなら、同じように予めそれらの行列を掛けておけば良いのだけど、ColorMatrix には行列の演算が用意されていないので、やるなら自前でやる他ない。

例11)印影を透過画像にする

ColorMatrix cm = new ColorMatrix(
    new float[][] {
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, -0.4f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, -0.4f, 0.0f },
        new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f },
        new float[] { 1.0f, 0.1f, 0.0f, 0.8f, 1.0f }
    }
);

// 白地に赤の印影があると想定。
// 赤一色にして、緑レベルと青レベルに応じてアルファ値を調節する。
// 色の薄いところを薄さに応じて半透過~透過にする感じ。
// 緑を0.1足して朱色っぽくした。
// アルファ値の最大は0.8なので、少し薄い感じになり、
// 最も色の濃いところでも、わずかに後ろが透けて見える。
印影を透過画像にする

以下、簡単なサンプルコード。

const string SrcImagePath = @"C:\Users\Public\Pictures\Sample Pictures\Pengins.jpg";

Image SrcImage;

using (FileStream InputStream = File.Open(SrcImagePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    SrcImage = Image.FromStream(InputStream);
}

int ImageWidth = SrcImage.Width;
int ImageHeight = SrcImage.Height;
Bitmap DstImage = new Bitmap(ImageWidth, ImageHeight);

using (Graphics g = Graphics.FromImage(DstImage))
{
    using (ImageAttributes ia = new ImageAttributes())
    {
        // ----
        ColorMatrix cm = new ColorMatrix(
            new float[][] {
                new float[] { 0.8f, 0.0f, 0.0f, 0.0f, 0.0f },
                new float[] { 0.0f, 0.8f, 0.0f, 0.0f, 0.0f },
                new float[] { 0.0f, 0.0f, 0.8f, 0.0f, 0.0f },
                new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
                new float[] { 0.2f, 0.2f, 0.2f, 0.0f, 1.0f }
            }
        );
        // ----

        ia.SetColorMatrix(cm);
        Rectangle DstRect = new Rectangle(0, 0, ImageWidth, ImageHeight);
        g.DrawImage(SrcImage, DstRect, 0, 0, ImageWidth, ImageHeight, GraphicsUnit.Pixel, ia);
    }
}

pictureBox1.Image = SrcImage;
pictureBox2.Image = DstImage;
(2018/08/03 初稿)
(2018/08/04 更新)
(2018/08/21 更新)
(2018/09/28 更新)