简体   繁体   中英

Remove similar Rectangles from a list

I have a List containing a few thousand rectangles. I'm trying to reduce the size of the list by removing items whose location is within x pixels of other items.

So far, my best attempt has been:

list = list.GroupBy(x => x.Location).Select(x => x.First()).ToList();

but this only removes exact matches. I'd love to remove all that are reasonably similar. Thoughts?

Thanks!

Trying (as well as I can) to follow @EricLippert's sage advice, and think about the data structures first.

WLOG, assume Location contains a Windows.System.Point . We create a couple of handy extensions to help us later:

public static class PointExt {
    public static double Distance(this Point p1, Point p2) => (p1-p2).Length;
    public static Point PointZero = new Point(0, 0);
}

Now we can define our Rectangle type (simplified for this problem):

public class Rectangle {
    public string Site;
    public Point Loc;

    public Rectangle() { }

    public Rectangle(string site, Point loc) {
        Site = site;
        Loc = loc;
    }
}

NOTE: Site is just to help in testing.

Now what we want to create are groups of Rectangle s that are all close to each other. I choose to define close as meaning within 5 units of the center of the ( Location s of the) group, found by averaging the current group's members.

So we can create a RectangleGroup class to help us with this definition:

public class RectangleGroup : IEnumerable<Rectangle> {
    List<Rectangle> members;
    Point center;

    public RectangleGroup() {
        members = new List<Rectangle>();
    }

    public RectangleGroup Add(Rectangle r) {
        members.Add(r);
        center = new Point(members.Average(m => m.Loc.X), members.Average(m => m.Loc.Y));
        return this;
    }

    public bool BelongsToGroup(Rectangle r) => center.Distance(r.Loc) <= 5;

    public Rectangle Middle() => members.OrderBy(m => m.Loc.Distance(center)).First();

    public IEnumerator<Rectangle> GetEnumerator() => members.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

I implemented IEnumerable on the group to make it possible to use LINQ with a RectangleGroup .

Using our tools in RectangleGroup , we can create a RectangleGroups class which manages a collection of RectangleGroup s similar to Lookup . This makes me think a (very) generic version of GroupBy could be created that delegates the group membership to a type could be useful, and would make this class unecessary.

public class RectangleGroups : IEnumerable<RectangleGroup> {
    List<RectangleGroup> groups;
    public RectangleGroups() {
        init();
    }

    public RectangleGroups(IEnumerable<Rectangle> rs) {
        init();

        foreach (var r in rs.OrderBy(r => r.Loc.Distance(PointExt.PointZero)))
            Add(r);
    }

    private void init() {
        groups = new List<RectangleGroup>();
    }

    public void Add(Rectangle r) {
        var found = false;
        foreach (var g in groups) {
            found = g.BelongsToGroup(r);
            if (found) {
                g.Add(r);
                break;
            }
        }
        if (!found)
            groups.Add(new LocationGroup().Add(r));
    }

    public IEnumerator<LocationGroup> GetEnumerator() => groups.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

With these classes available, reducing the List<Rectangle> to just each Rectangle nearest the center of each group is trivial:

var ans = new RectangleGroups(list).Select(lg => lg.Middle());

I think you're on the right track. Think about how you're generating keys for the group by. Maybe generate keys based on close-ness rather than exact match.

list = list.GroupBy(x => ApproximateLocation(x.Location)).Select(x => x.First()).ToList();


public Location ApproximateLocation(Location original)
{
  int precision = 5;
  Location result = new Location{
    X = (original.X / precision) * precision;
    Y = (original.Y / precision) * precision;

  };
  return result;
}

You can define a custom comparison function and use it in the GroupBy clause.

using System.Collections.Generic;
using System.Linq;

list.GroupBy(x => x.Location, new CustomComparer())...

public class CustomComparer : IEqualityComparer<LocationType>
{
    public bool Equals(LocationType A, LocationType B)
    {
        //return true if A is close enough to B
    }
}

This article goes into great detail: https://dotnetcodr.com/2014/06/20/grouping-elements-in-linq-net-using-groupby-and-an-equalitycomparer/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM