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 を設定したほうが良い。

例1)各色を80%にして、0.2底上げする → 明るくする。

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 }
    }
);

// コンストラクタの引数はジャグ配列になっているので、注意しよう。
fig-1
例2)グレイスケールに変換する。

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 に載っていたのを無暗に信用して採用した。
fig-2
例3)半透明(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%透過になる。そんな感じ。
fig-3
例4)ネガにする。

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 }
    }
);

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

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の方向へ半回転させ色相を変換する。
// 何かの役に立つとは思えない。
fig-5
例6)色相を入れ替える

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 }
    }
);

// 例5を拡張したもの。任意の回転数で色相を変換する。
// 例5はValue=0.5の場合。
fig-6

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

const string SrcImagePath = @"C:\Users\Public\Pictures\Sample Pictures\Autumn Leaves.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, PixelFormat.Format24bppRgb);

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;

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

(2018/08/03 初稿)
(2018/08/04 更新)