【C#】2次元配列から2次元配列を切り出す

タイトルの通りなのですが、絵にするとこんな感じです。

以下のような2次元配列があった時に

f:id:Takachan:20200220221712p:plain

以下のようにある部分を範囲選択して切り取り新しい2次元配列を作成する処理になります。

f:id:Takachan:20200220221727p:plain

以下コードですが最新版なら普通のC#でもUnityでもどちらでも行けます。

実装コード

早速実装例を紹介したいと思います。

ArrayUtilityクラス

まず、2次元配列に対する操作を行うUtilityクラスを以下の通り作成します。

冒頭の図のような処理を行うのは中ほどにあるCutメソッドです。

中心座標を指定してその周囲を切り取るCutByCenterも併せて実装しました。

// 配列に対する汎用機能を提供します。
public static class ArrayUtility
{
    // 指定した2次元配列を複製します。
    public static T[,] Clone<T>(T[,] src)
    {
        int h = src.GetLength(0);
        int w = src.GetLength(1);
        var map = new T[h, w];
        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                map[y, x] = src[y, x];
            }
        }
        return map;
    }

    // 指定した2次元配列の位置 (x, y) から (w to h) の範囲を切り取ります。
    // (w, h)の 指定が src の範囲を超える場合 src の範囲内で切り取りを行います。
    public static T[,] Cut<T>(T[,] src, int x, int y, int w, int h)
    {
        // コピー元の配列の大きさ
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);

        // 入力値が範囲内に収まるかどうか
        if (x < 0 || x > xmax || y < 0 || y > ymax)
        {
            string msg = $"Parameter is out of range. src[y={ymax},x={xmax}], x={x}, y={y}";
            throw new ArgumentOutOfRangeException(msg);
        }
        if (w == 0 || h == 0)
        {
            throw new ArgumentException($"Invalid parameter. w={w}, h={h}");
        }

        // コピー先の配列の大きさ
        int rh = y + h <= ymax ? h : h - (y + h - ymax);
        int rw = x + w <= xmax ? w : w - (x + w - xmax);
        
        // 元の大きさからコピーする
        var map = new T[rh, rw];
        for (int my = 0, _y = y; my < rh; my++, _y++)
        {
            for (int mx = 0, _x = x; mx < rw; mx++, _x++)
            {
                map[my, mx] = src[_y, _x];
            }
        }

        return map;
    }

    // 2次元配列の中心位置 (cx, cy) から指定した半径 (rw, rh) の範囲を切り取ります。
    // (rw, rh) が src の範囲を超える場合 src の範囲内で切り取りを行います。
    // 
    // 戻り値のタプルには src から切り取った範囲の値が設定されます。
    public static (T[,] map, Rect2Di rect) CutByCenter<T>(T[,] src, int cx, int cy, int rw, int rh)
    {
        int xmax = src.GetLength(1);
        int ymax = src.GetLength(0);

        if (cx < 0 || cx > xmax || cy < 0 || cy > ymax)
        {
            string msg = $"Parameter is out of range. src[y={ymax},x={xmax}], x={cx}, y={cy}";
            throw new ArgumentOutOfRangeException(msg);
        }

        // 切り取る範囲の最大・最小値を取得
        int min_x = cx - rw;
        int max_x = cx + rw;
        int min_y = cy - rh;
        int max_y = cy + rh;
        if (min_x < 0)
        {
            min_x = 0;
        }
        if (max_x >= xmax)
        {
            max_x = xmax - 1;
        }
        if (min_y < 0)
        {
            min_y = 0;
        }
        if (max_y >= ymax)
        {
            max_y = ymax - 1;
        }

        var map = new T[max_y - min_y + 1, max_x - min_x + 1];

        for (int my = 0, _y = min_y; _y <= max_y; my++, _y++)
        {
            for (int mx = 0, _x = min_x; _x <= max_x; mx++, _x++)
            {
                map[my, mx] = src[_y, _x];
            }
        }

        return (map, new Rect2Di(min_x, max_x, min_y, max_y));
    }

    // [デバッグ用] 指定した配列の内容を文字列に変換します。
    public static string TostringByDebug<T>(T[,] src)
    {
        var a = new StringBuilder();
        var b = new StringBuilder();
        int ymax = src.GetLength(0);
        int xmax = src.GetLength(1);
        for (int y = 0; y < ymax; y++)
        {
            for (int x = 0; x < xmax; x++)
            {
                b.Append($" {src[y,x]},");
            }
            a.Append(b.ToString().Trim(' ', ','));
            a.Append(Environment.NewLine);
            b.Clear();
        }
        return a.ToString();
    }
}

// 一時的なデータの入れ物
public readonly struct Rect2Di
{
    public int XMin { get; }
    public int YMin { get; }
    public int XMax { get; }
    public int YMax { get; }
    public Rect2Di(int xmin, int ymin, int xmax, int ymax)
    {
        this.XMin = xmin;
        this.YMin = ymin;
        this.XMax = xmax;
        this.YMax = ymax;
    }
}

使い方

上記実装の使用方法です。

public static void Main(string[] args)
{
    int[,] _m = new int[,]
    {
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
        { 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 2, 2, 2, 2, 2, 3, 3, 3, 3, 3 },
        { 3, 3, 3, 3, 3, 4, 4, 4, 4, 4 },
        { 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 },
        { 5, 5, 5, 5, 5, 6, 6, 6, 6, 6 },
        { 6, 6, 6, 6, 6, 7, 7, 7, 7, 7 },
        { 7, 7, 7, 7, 7, 8, 8, 8, 8, 8 },
        { 8, 8, 8, 8, 8, 9, 9, 9, 9, 9 },
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
    };

    // 左上基準に範囲を切り取る
    var map1 = ArrayUtility.Cut(_m, 0, 0, 5, 5);
    Console.WriteLine(map1.ToStringByDebug());
    Console.WriteLine("");
    // 0, 0, 0, 0, 0
    // 1, 1, 1, 1, 1
    // 2, 2, 2, 2, 2
    // 3, 3, 3, 3, 3
    // 4, 4, 4, 4, 4


    // 中心座標を指定して範囲を切り取る
    var (map, rect) = ArrayUtility.CutByCenter(_m, 4, 4, 2, 2);
    Console.WriteLine(map.ToStringByDebug());
    Console.WriteLine("");
    
    // (4, 4)を中心に上下左右に2ずつ(縦横5x5)の範囲を切り取る
    // 2, 2, 2, 3, 3
    // 3, 3, 3, 4, 4
    // 4, 4, 4, 5, 5
    // 5, 5, 5, 6, 6
    // 6, 6, 6, 7, 7
}

ArrayExtension:拡張メソッド版

上記の処理を2次元配列の拡張メソッドとして定義したいと思います。

コード例は以下の通りです。

先ほどのUtilityの処理を中で呼び出すようにしています。

// 配列に対する拡張機能を提供します。
public static class ArrayExtension
{
    // <see cref="ArrayUtility.Clone{T}(T[,])"/> と同じ機能を持つ拡張メソッド
    public static T[,] Clone<T>(this T[,] src) => ArrayUtility.Clone(src);

    // <see cref="ArrayUtility.Cut{T}(T[,], int, int, int, int)"/> と同じ機能を持つ拡張メソッド
    public static T[,] Cut<T>(this T[,] src, int x, int y, int w, int h)
    {
        ArrayUtility.Cut(src, x, y, w, h);
    }

    // <see cref="ArrayUtility.CutByCenter{T}(T[,], int, int, int, int)"/> と同じ機能を持つ拡張メソッド
    public static (T[,] map, Rect2Di rect) CutByCenter<T>(this T[,] src, int cx, int cy, int rw, int rh)
    {
        ArrayUtility.CutByCenter(src, cx, cy, rw, rh);
    }

    // <see cref="ArrayUtility.TostringByDebug{T}(T[,])"/> と同じ機能を持つ拡張メソッド
    public static string ToStringByDebug<T>(this T[,] src)
    {
        ArrayUtility.TostringByDebug(src);
    }
}

使い方

上記実装例の使用方法です。

ほぼ同じですが、2次元配列のメソッドとして使用できるようになっています。多分こっちの方がすっきりすると思います。

public static void Main(string[] args)
{
    int[,] _m = new int[,]
    {
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
        { 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 2, 2, 2, 2, 2, 3, 3, 3, 3, 3 },
        { 3, 3, 3, 3, 3, 4, 4, 4, 4, 4 },
        { 4, 4, 4, 4, 4, 5, 5, 5, 5, 5 },
        { 5, 5, 5, 5, 5, 6, 6, 6, 6, 6 },
        { 6, 6, 6, 6, 6, 7, 7, 7, 7, 7 },
        { 7, 7, 7, 7, 7, 8, 8, 8, 8, 8 },
        { 8, 8, 8, 8, 8, 9, 9, 9, 9, 9 },
        { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 },
    };

    // 左上基準に範囲を切り取る
    var map1 = _m.Cut(0, 0, 5, 5);
    Console.WriteLine(map1.ToStringByDebug());
    Console.WriteLine("");
    // 0, 0, 0, 0, 0
    // 1, 1, 1, 1, 1
    // 2, 2, 2, 2, 2
    // 3, 3, 3, 3, 3
    // 4, 4, 4, 4, 4


    // 中心座標を指定して範囲を切り取る
    var (map, rect) = _m.CutByCenter(4, 4, 2, 2);
    Console.WriteLine(map.ToStringByDebug());
    Console.WriteLine("");
    
    // (4, 4)を中心に上下左右に2ずつ(縦横5x5)の範囲を切り取る
    // 2, 2, 2, 3, 3
    // 3, 3, 3, 4, 4
    // 4, 4, 4, 5, 5
    // 5, 5, 5, 6, 6
    // 6, 6, 6, 7, 7
}

こちらも最初のコードと同じように切り取ることができました。

コードばかりになってしまいましたが以上です。