4

Given the following c# array:

    const bool X = true, _ = false;
    bool[,] array = new bool[,] {
        { X, X, X, X, _, X, X, X, _, _ },
        { _, _, _, _, _, X, X, X, X, _ },
        { X, X, X, X, X, X, X, X, X, X },
        { X, X, X, X, X, X, X, X, X, X },
        { _, X, X, X, _, X, _, _, X, X },
        { _, X, X, X, _, X, X, _, X, X },
        { _, _, _, X, X, _, _, _, _, _ },
        { X, X, _, X, X, _, X, X, X, _ },
        { _, X, X, _, _, X, X, X, X, X },
        { _, _, X, X, _, _, X, X, X, X },
    };

Visually something like this:

Array Block Diagram

Are there any existing methods for converting this to as few rectangular areas as possible?

A possible solution might be something like:

Rectangle Block Diagram

Ideally I'm looking for something that can produce a list of rectangles:

    public IEnumerable<Rectangle> ReduceMap(bool[,] map)
    {
        List<Rectangle> rects = new List<Rectangle>();
        int width = map.GetLength(0), height = map.GetLength(1);

        // Reduce
        // ....?

        return rects;
    }

The result of which would be something like:

    var output = new List<Rectangle>
    {
        new Rectangle(0, 0, 4, 1),
        new Rectangle(5, 0, 3, 2),
        new Rectangle(8, 1, 1, 1),
        new Rectangle(0, 2, 10, 2),
        // ... etc
    };

Speed is important too, the fewer rectangles the better, but finding a decent solution should not be overly computationally expensive.

Apologies if this has been answered before, if anyone has any idea where to look that would be great!

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Rob
  • 1,687
  • 3
  • 22
  • 34
  • Just to understand the requirements: What problem are you trying to solve? What's the application? – Fildor Aug 26 '20 at 13:07
  • 1
    Methinks this is an NP-hard problem for *perfect*-solutions. – Dai Aug 26 '20 at 13:10
  • 1
    @Fildor: I'm trying to glue together a chunked-group of known rectangular polygons for use in a dynamic lighting shadow map algorithm applied to a tile-map. It's too expensive to hull all the tiles individually, reducing this space would allow for dynamic recalculation. – Rob Aug 26 '20 at 13:13
  • @Dai: agreed, although a perfect solution is not required. – Rob Aug 26 '20 at 13:13
  • 3
    This sounds like a quadtree compression. – Matthew Watson Aug 26 '20 at 13:14
  • My first idea would be: 1) find a list of rectangles that (very redundantly) covers the map; 2) use a set-cover algorithm to find a solution using these rectangles. Take a look at https://en.wikipedia.org/wiki/Geometric_set_cover_problem – Stef Aug 26 '20 at 13:14

2 Answers2

1

This is an interesting problem.
Have you seen the following article it provides good information on this topic and discusses different approaches to this problem (quadtree, morphological, graph): Rectangular decomposition of binary images
This comes from Ojdo's answer in this question Link
I would start looking there! Hope this helped.

0

This is the best I can come up with myself, if anyone can improve on this, I'd be happy to mark it as the answer.

This method uses multiple passes to union the array and seems to reduce the array pretty well, roughly in line with my diagrams above, although definitely not optimal.

I've created a fiddle for testing: https://dotnetfiddle.net/3iuIe6

using System;
using System.Collections.Generic;
using System.Drawing;

public class Program
{
    private const string pattern1 = @"
XXXX_XXX__
_____XXXX_
XXXXXXXXXX
XXXXXXXXXX
_XXX_X__XX
_XXX_XX_XX
___XX_____
XX_XX_XXX_
_XX__XXXXX
__XX__XXXX
";

    private const string pattern2 = @"
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
";

    private const string pattern3 = @"
X_XXXXXXXX
X_XXXXXXXX
X_XXXXXXXX
X_XXXXXXXX
XXXXXXXXXX
XXXXXXXXXX
XXXXXXX_XX
XXXXXXX_XX
XXXXXXX_XX
XXXXXXX_XX
";

    private const string pattern4 = @"
X_XXXXXXXX
X_XX__XXXX
X_XXXXXXXX
X_XXXX__XX
XXXXX_XXXX
XXX__XXXXX
XXXX_XX_XX
XXX_XXX_XX
XXXXX_X_XX
XXX_XXX_XX
";

    private const string pattern5 = @"
XXXXXXXXXX
__XXXXXXXX
____XXXXXX
_____XXXXX
______XXXX
______XXXX
_____XXXXX
____XXXXXX
__XXXXXXXX
XXXXXXXXXX
";

    public static void Main()
    {
        bool[,] map = PatternTo2DArray(pattern1);
        //bool[,] map = PatternTo2DArray(pattern2);
        //bool[,] map = PatternTo2DArray(pattern3);
        //bool[,] map = PatternTo2DArray(pattern4);
        //bool[,] map = PatternTo2DArray(pattern5);

        var rects = ReduceMap(map);
        int index = 0;
        foreach (var rect in rects)
        {
            Console.WriteLine("{0}: {{ {1}, {2}, {3}, {4} }}", ((index + 1).ToString().PadLeft(1, '0')), rect.X, rect.Y, rect.Width, rect.Height);
            index++;
        }

        var total = DumpMap(map);
        Console.WriteLine("\r\nOptimised Map From: {0} to {1}", total, index);
    }

    public static IEnumerable<Rectangle> ReduceMap(bool[,] map)
    {
        int width = map.GetLength(0), height = map.GetLength(1);
        MapElement[,] bin = new MapElement[width, height];

        // Reduce
        // Step 1: Convert to map elements:
        for (int y = 0; y < height; y++)
            for (int x = 0; x < width; x++)
            {
                if (map[x, y])
                    bin[x, y] = new MapElement() { X = x, Y = y, Width = 1, Height = 1, Set = true };
            }

        // Step 2: Process the bin map and generate a collection of Rectangles horizontally     
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Only care about this if we are set.
                if (bin[x, y].Set)
                {
                    // Scan forward and link this tile with its following tiles
                    int xx = 0;
                    for (int xForward = x + 1; xForward < width; xForward++)
                    {
                        if (!bin[xForward, y].Set)
                            break;

                        // We can link this...
                        bin[xForward, y].Set = false;
                        bin[x, y].Width++;
                        xx++; // Skip over these tiles.
                    }

                    x += xx;
                }
            }
        }

        // Step 3: Process the bin map veritically and join any blocks that have equivalent blocks below them.
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Only care about this if we are set.
                if (bin[x, y].Set)
                {
                    // Scan down and link this tile with its following tiles
                    for (int yDown = y + 1; yDown < height; yDown++)
                    {
                        if (!bin[x, yDown].Set || bin[x, yDown].Width != bin[x, y].Width)  // We might be able to link this if it's the same size
                            break;

                        bin[x, yDown].Set = false;
                        bin[x, y].Height++;
                    }
                }
            }
        }

        // Step 4: Convert map to rectangles
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Only care about this if we are set.
                if (bin[x, y].Set)
                {
                    var b = bin[x, y];
                    yield return new Rectangle(b.X, b.Y, b.Width, b.Height);
                }
            }
        }
    }

    public static int DumpMap(bool[,] map)
    {
        Console.WriteLine("");
        var @out = 0;
        int width = map.GetLength(0), height = map.GetLength(1);
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                Console.Write(map[x, y] ? "X" : "_");
                @out++;
            }

            Console.WriteLine();
        }

        return @out;
    }

    public static int CountDataPoints(bool[,] map)
    {
        var @out = 0;
        int width = map.GetLength(0), height = map.GetLength(1);
        for (int y = 0; y < height; y++)
            for (int x = 0; x < width; x++)
                if (map[x, y])
                    @out++;

        return @out;
    }

    public static bool[,] PatternTo2DArray(string pattern)
    {
        var lines = new List<string>(pattern.Split('\n'));
        for (int i = lines.Count - 1; i > -1; i--)
        {
            string line = lines[i].TrimEnd('\r');
            if (line.Length == 0)
                lines.RemoveAt(i);
            else
                lines[i] = line;
        }

        var @out = new bool[lines[0].Length, lines.Count];
        for (int y = 0; y < lines.Count; y++)
        {
            string line = lines[y];
            for (int x = 0; x < line.Length; x++)
            {
                @out[x, y] = !(line[x] == '_');
            }
        }

        return @out;
    }

    internal struct MapElement
    {
        public int X;
        public int Y;
        public int Width;
        public int Height;
        public bool Set;
    }
}

Rob
  • 1,687
  • 3
  • 22
  • 34