将水平和垂直线分组到表中(C#)

时间:2012-09-14 11:47:42

标签: c# algorithm

一个简单的线类被定义为包含起点和终点坐标的两个PointF成员:

public class Line    {
    PointF s, e;
}

我有两个列表,其中包含出现在绘图画布上并形成一个或多个表格的所有水平和垂直线。

List<Line> AllHorizontalLines;
List<Line> AllVerticalLines;

我需要对这些行进行分组,以便属于一个表的行被捕获在一个组中,因此分组函数将具有如下签名:

List<List<Line>> GroupLines(List<Line> hor, List<Line> ver)
{ 

}

为简单起见,我们假设页面上只有“简单”表,即没有嵌套表。但是可以有合并的单元格,因此我们必须忽略小于父表的完整高度的小水平和垂直线。为了进一步简化,假设两个输入列表都已排序(水平线w.r.t. Y轴和垂直线w.r.t.X轴)。

有没有已知的算法来解决这个问题?或者任何人都可以帮我设计一个?

2 个答案:

答案 0 :(得分:1)

以下似乎有效:

  • 设置一个字典,将边界矩形映射到每个矩形中的行列表。
  • 对于两个输入列表中的每一行(我们不关心方向)
    • 从行
    • 中创建一个边界矩形
    • 检查线是否穿过任何现有的边界矩形。
      • 如果是这样,请合并这些矩形中的线条,添加当前线条,删除触摸的矩形,计算新的边界矩形,然后再次检查。
      • 否则,将此新矩形添加到字典中并删除旧矩形。
  • 返回每个矩形的行列表。

这是我得到的代码:

public static IEnumerable<IEnumerable<Line>> GroupLines(IEnumerable<Line> lines)
{
    var grouped = new Dictionary<Rectangle, IEnumerable<Line>>();

    foreach (var line in lines)
    {
        var boundedLines = new List<Line>(new[] { line });
        IEnumerable<Rectangle> crossedRectangles;
        var boundingRectangle = CalculateRectangle(boundedLines);
        while (
            (crossedRectangles = grouped.Keys
                .Where(r => Crosses(boundingRectangle, r))
                .ToList()
            ).Any())
        {
            foreach (var crossedRectangle in crossedRectangles)
            {
                boundedLines.AddRange(grouped[crossedRectangle]);
                grouped.Remove(crossedRectangle);
            }
            boundingRectangle = CalculateRectangle(boundedLines);
        }
        grouped.Add(boundingRectangle, boundedLines);
    }
    return grouped.Values;
}

public static bool Crosses(Rectangle r1, Rectangle r2)
{
    return !(r2.Left > r1.Right ||
        r2.Right < r1.Left ||
        r2.Top > r1.Bottom ||
        r2.Bottom < r1.Top);
}

public static Rectangle CalculateRectangle(IEnumerable<Line> lines)
{
    Rectangle rtn = new Rectangle
    {
        Left = int.MaxValue,
        Right = int.MinValue,
        Top = int.MaxValue,
        Bottom = int.MinValue
    };

    foreach (var line in lines)
    {
        if (line.P1.X < rtn.Left) rtn.Left = line.P1.X;
        if (line.P2.X < rtn.Left) rtn.Left = line.P2.X;
        if (line.P1.X > rtn.Right) rtn.Right = line.P1.X;
        if (line.P2.X > rtn.Right) rtn.Right = line.P2.X;
        if (line.P1.Y < rtn.Top) rtn.Top = line.P1.Y;
        if (line.P2.Y < rtn.Top) rtn.Top = line.P2.Y;
        if (line.P1.Y > rtn.Bottom) rtn.Bottom = line.P1.Y;
        if (line.P2.Y > rtn.Bottom) rtn.Bottom = line.P2.Y;
    }

    return rtn;
}

WinForms app I knocked together

答案 1 :(得分:1)

以下是我建议的方法:

  • 清除两个列表,以便没有任何完全包含的“小”行。
  • 选择任何一行。
  • 取出与此线接触(相交)的所有线条。
  • 对于这些行中的每一行,请使用触摸它们的所有行。
  • 继续,直到找不到更多感人的线条。
  • 你现在有一个小组。
  • 从剩余的行中选择一行,然后重复,直到没有剩余行。

<强>代码:

public static IEnumerable<IEnumerable<Line>> Group(IEnumerable<Line> horizontalLines, IEnumerable<Line> verticalLines)
{
  // Clean the input lists here. I'll leave the implementation up to you.

  var ungroupedLines = new HashSet<Line>(horizontalLines.Concat(verticalLines));
  var groupedLines = new List<List<Line>>();

  while (ungroupedLines.Count > 0)
  {
    var group = new List<Line>();
    var unprocessedLines = new HashSet<Line>();
    unprocessedLines.Add(ungroupedLines.TakeFirst());

    while (unprocessedLines.Count > 0)
    {
      var line = unprocessedLines.TakeFirst();
      group.Add(line);
      unprocessedLines.AddRange(ungroupedLines.TakeIntersectingLines(line));
    }

    groupedLines.Add(group);
  }

  return groupedLines;
}

public static class GroupingExtensions
{
  public static T TakeFirst<T>(this HashSet<T> set)
  {
    var item = set.First();
    set.Remove(item);
    return item;
  }

  public static IEnumerable<Line> TakeIntersectingLines(this HashSet<Line> lines, Line line)
  {
    var intersectedLines = lines.Where(l => l.Intersects(line)).ToList();
    lines.RemoveRange(intersectedLines);
    return intersectedLines;
  }

  public static void RemoveRange<T>(this HashSet<T> set, IEnumerable<T> itemsToRemove)
  {
    foreach(var item in itemsToRemove)
    {
      set.Remove(item);
    }
  }

  public static void AddRange<T>(this HashSet<T> set, IEnumerable<T> itemsToAdd)
  {
    foreach(var item in itemsToAdd)
    {
      set.Add(item);
    }
  }
}

将此方法添加到Line

public bool Intersects(Line other)
{
  // Whether this line intersects the other line or not.
  // I'll leave the implementation up to you.
}

备注:

如果此代码运行得太慢,您可能需要水平扫描,随时选择连接的线路。可能还值得看this

<强>专门:

public static IEnumerable<IEnumerable<Line>> Group(IEnumerable<Line> horizontalLines, IEnumerable<Line> verticalLines)
{
  // Clean the input lists here. I'll leave the implementation up to you.

  var ungroupedHorizontalLines = new HashSet<Line>(horizontalLines);
  var ungroupedVerticalLines = new HashSet<Line>(verticalLines);
  var groupedLines = new List<List<Line>>();

  while (ungroupedHorizontalLines.Count + ungroupedVerticalLines.Count > 0)
  {
    var group = new List<Line>();
    var unprocessedHorizontalLines = new HashSet<Line>();
    var unprocessedVerticalLines = new HashSet<Line>();

    if (ungroupedHorizontalLines.Count > 0)
    {
      unprocessedHorizontalLines.Add(ungroupedHorizontalLines.TakeFirst());
    }
    else
    {
      unprocessedVerticalLines.Add(ungroupedVerticalLines.TakeFirst());
    }

    while (unprocessedHorizontalLines.Count + unprocessedVerticalLines.Count > 0)
    {
      while (unprocessedHorizontalLines.Count > 0)
      {
        var line = unprocessedHorizontalLines.TakeFirst();
        group.Add(line);
                unprocessedVerticalLines.AddRange(ungroupedVerticalLines.TakeIntersectingLines(line));
      }
      while (unprocessedVerticalLines.Count > 0)
      {
        var line = unprocessedVerticalLines.TakeFirst();
        group.Add(line);
        unprocessedHorizontalLines.AddRange(ungroupedHorizontalLines.TakeIntersectingLines(line));
      }
    }
    groupedLines.Add(group);
  }

  return groupedLines;
}

这假设没有线重叠,因为它不检查水平线是否接触其他水平线(垂直相同)。

您可以删除if-else。只是存在垂直线没有连接到水平线的情况。