2022年1月8日土曜日

C#でIronPalette

 C#、System名前空間で近似値のインデックスを二分木で探す機能が用意されているのを知ったので、試しにヒートマップ用のカラーパレットを持つ機能を作ってみた。元ネタは4年前くらいに作ったやつ。以前のコードは呼び出すたびに三角関数を3回使うので解像度を増やすと処理が遅くなる欠点があった。


 先に使い方。

const int w = 10;

const int h = 10;

const int tileW = 50;

const int tileH = 50;

var palette = new IronPalette<int>(Enumerable.Range(-1, w * h).Select(i => (i, (double)i / (w * h - 3))), new Comparer());


var bmp = new Bitmap(w * tileW, h * tileH);

using (var g = Graphics.FromImage(bmp))

using (var pen = new Pen(Color.White))

{

    for (int y = 0; y < h; y++)

    {

        for (int x = 0; x < w; x++)

        {

            pen.Color = palette[w * y + x - 1];

            g.FillRectangle(pen.Brush, x * tileW, y * tileH, tileW, tileH);

        }

    }

}

// class Comparer : IComparer<int> { public int Compare(int x, int y) => x.CompareTo(y); }

 みたいなコードを書くと

 みたいな画像になる。

 標準でクリップされるので、端を1個ずつ飛ばすシーケンスを渡しておけば範囲外をサチらせられる(両端を白黒にしない場合でも、デフォルトでサチるので、範囲外で例外投げる必要があったりする場合は呼び出し側で対応する必要がある)。

 今回は98色(+サチ)でテーブルを作っているので値が変わると色が違うのがみえるけど、500色くらいで近似すればほとんど段差を感じずに見えるようになる。ただし白黒に落とすと情報量が減る分で段差が強調される。

 IComparerを渡すので、比較できる値なら何でも渡すことができる。以前のようにdoubleの0-1に正規化する必要がないので簡単に使えるようになった。/* 組み込み型のIComparerって用意されてないのかな? 地味に不便。 */

 System.Array.BinarySearchで近似値取れるのめちゃくちゃ便利。

 正確には近似ではなく、負の無限大に丸めた値が出てくるので、例えば{ 1, 2 }に対して1.9を探すと2ではなく1が得られる。


 本体のコード

class IronPalette<T>

{

    private readonly T[] _table1;

    private readonly Color[] _table2;

    private readonly IComparer<T> _comparer;


    public IronPalette(IEnumerable<(T, double)> seq, IComparer<T> comparer)

    {

        var tmp =

            seq

            .Select(a => (a.Item1, a.Item2 < 0 ? Color.Black : 1 < a.Item2 ? Color.White : ValueToColor(a.Item2)))

            .OrderBy(a => a.Item1, comparer)

            .ToArray();


        _table1 = new T[tmp.Length];

        _table2 = new Color[tmp.Length];

        _comparer = comparer;


        for (int i = 0; i < tmp.Length; i++)

        {

            _table1[i] = tmp[i].Item1;

            _table2[i] = tmp[i].Item2;

        }

    }


    public Color this[T value]

    {

        get

        {

            var a = Array.BinarySearch(_table1, value, _comparer);

            if (a < 0) { a = a == -1 ? 0 : (~a - 1); }

            return _table2[a];

        }

    }


    public static Color ValueToColor(double value)

    {

        const float Rf = 1.5879f;

        const float Gf = 4.2124f;

        const float Bf = 2.2251f;


        const float Rp = 3.161f;

        const float Gp = 0.8156f;

        const float Bp = 3.1285f;


        const float Rg = 0.4042f;

        const float Gg = 1.0976f;

        const float Bg = 1.9342f;


        const float Ro = 0.0027f;

        const float Go = -1.5174f;

        const float Bo = 1.9635f;


        const float Ra = 1.9869f;

        const float Ga = -0.1639f;

        const float Ba = 2.2419f;


        const float Rb = -3.9506f;

        const float Gb = -0.17f;

        const float Bb = 0.1099f;


        if (value < 0 || 1 < value) { throw new ArgumentOutOfRangeException(); }


        return Color.FromArgb(

            (int)((Math.Sin(value * Rf + Rp) * Rg + Ro) * (value * Ra + Rb) * 255),

            (int)((Math.Sin(value * Gf + Gp) * Gg + Go) * (value * Ga + Gb) * 255),

            (int)((Math.Sin(value * Bf + Bp) * Bg + Bo) * (value * Ba + Bb) * 255));

    }

}


2022-01-08 fix: 2の補数の計算を修正


/* このブログでプログラミングの話題出すのめちゃくちゃ久しぶりじゃないか。。。 */

0 件のコメント:

コメントを投稿