GithubHelp home page GithubHelp logo

mashin6 / orthogonalize-polygon Goto Github PK

View Code? Open in Web Editor NEW
25.0 4.0 4.0 3.19 MB

Orthogonalize polygon in python by making all its angles 90 or 180 deg

License: GNU General Public License v3.0

Python 100.00%

orthogonalize-polygon's Introduction

Orthogonalize Polygon in python

This script performs squaring/orthogonalization of polygons, in other words making all its angles 90˚ or 180˚, which is usefull for automated adjustment of building footprint shapes. Script uses GeoPandas and Shapely packages and is an improved implementation of JOSM Orthogonalize function. Supports Polygons with internal holes as well as Multipolygons and a typical polygon takes on Core i7-6660U 2.4GHz about ~0.34s processing time.

Features

  • Supports Polygons with internal rings/holes.
  • Supports MultiPolygons
  • Skew angle tolerance to preserve angled features such as bay windows.
  • Adjustable range of angles that determines whether polygon edge follows the same direction as previous segment or rather turns 90˚.

Preserving angled features

Comparison to JOSM

Example:

orthogonalize-polygon's People

Contributors

mashin6 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

orthogonalize-polygon's Issues

Question about the workflow

Hi, thanks for your great job.
I tried it with my geo-data, and it really performs well. However, it's hard for me to understant the whole workflow, especially the roration.
Any paper can I read about to know more?
Thanks~

Pip package availability ?

Hi , I loved your implementation , Is there a pip package available that I can use on other projects by importing certain functions ? Or do you recommend to fork / create new with credits ? I see license requirements : GPL 3

Be aware of CRS

Thank you for the wonderful code. I have a deep learning model trained for building detection and this repo had exactly what I needed to create presentable polygons. The only thing I will say is that you need to be careful with CRS. If you are working with polygons in a specific CRS that is not WGS84, you will receive an error. As referenced below, WGS84 is hard-coded into the algorithm, so please be aware that you will need to change these hard-coded CRS values or reproject your original data for the operation to work.

# Create WGS84 referenced GeoSeries
bS = gpd.GeoDataFrame({'geometry':[polySimple]})
bS.crs = "EPSG:4326"
# Temporary reproject to Merkator and rotate by median angle
bSR = bS.to_crs('epsg:3857')
bSR = bSR.rotate(angle, origin='centroid', use_radians=False)
bSR = bSR.to_crs('epsg:4326')

Convert To C#

`using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[ExecuteAlways]
public class Squarer : MonoBehaviour
{
public List originalPolygonVertices;
public float maxAngleChange = 10f;
public float skewTolerance = 10f;

void OnDrawGizmos()
{
    Bounds boundingBox = CalculateBoundingBox(originalPolygonVertices);
    DrawBoundingBox(boundingBox);
    DrawPolygon(originalPolygonVertices, Color.green);
    DrawPolygon(OrthPolygon(originalPolygonVertices, maxAngleChange, skewTolerance), Color.magenta);

    //   Debug.LogError(.Count);

    // Handle the returned values as needed
    /*Debug.Log("Original Angles: " + string.Join(", ", orgAngles));
    Debug.Log("Correction Angles: " + string.Join(", ", corAngles));
    Debug.Log("Directional Angles: " + string.Join(", ", dirAngles));*/
}


public List<Vector2> OrthPolygon(List<Vector2> polygon, float maxAngleChange = 15f, float skewTolerance = 15f)
{
    List<Vector2> poly = new List<Vector2>();
    List<float> orgAngles;
    List<float> corAngles;
    List<int> dirAngles;
    (orgAngles, corAngles, dirAngles) = CalculateSegmentAngles(originalPolygonVertices);

    float medAngle = 0;

    if (StandardDeviation(corAngles) < 30)
    {
        medAngle = Median(corAngles);
    }
    else
    {
        medAngle = 45;
    }

    List<Vector2> polySimpleR = RotatePolygon(polygon, medAngle);
    (orgAngles, corAngles, dirAngles) = CalculateSegmentAngles(polySimpleR, maxAngleChange);

    List<float> rotatedX = new List<float>();
    List<float> rotatedY = new List<float>();

    foreach (Vector2 vertex in polySimpleR)
    {
        rotatedX.Add(vertex.x);
        rotatedY.Add(vertex.y);
    }

    // Scan backwards to check if starting segment is a continuation of straight region in the same direction
    int shift = 0;
    for (int i = 1; i < dirAngles.Count; i++)
    {
        if (dirAngles[0] == dirAngles[^i])
        {
            shift = i;
        }
        else
        {
            break;
        }
    }

    // If the first segment is part of continuing straight region then reset the index to its beginning
    if (shift != 0)
    {
        dirAngles = dirAngles.GetRange(dirAngles.Count - shift, shift)
            .Concat(dirAngles.GetRange(0, dirAngles.Count - shift)).ToList();
        orgAngles = orgAngles.GetRange(orgAngles.Count - shift, shift)
            .Concat(orgAngles.GetRange(0, orgAngles.Count - shift)).ToList();
        rotatedX = rotatedX.GetRange(rotatedX.Count - shift - 1, shift)
            .Concat(rotatedX.GetRange(0, rotatedX.Count - shift)).ToList();
        rotatedY = rotatedY.GetRange(rotatedY.Count - shift - 1, shift)
            .Concat(rotatedY.GetRange(0, rotatedY.Count - shift)).ToList();
    }

    // Fix 180 degree turns (N->S, S->N, E->W, W->E)
    // Subtract two adjacent directions and if the difference is 2, which means we have 180˚ turn (0,1,3 are OK) then use the direction of the previous segment
    List<int> dirAngleRoll = dirAngles.GetRange(1, dirAngles.Count - 1)
        .Concat(dirAngles.GetRange(0, 1)).ToList();
    dirAngles = dirAngles.Select((value, index) => MathF.Abs(value - dirAngleRoll[index]) == 2 ? dirAngles[index - 1] : value).ToList();


    // Assuming dirAngle, orgAngle, rotatedX, rotatedY, skewTolerance, and shift are defined elsewhere in the code
    dirAngles.Add(dirAngles[0]); // Append dummy value
    orgAngles.Add(orgAngles[0]); // Append dummy value
    List<int> segmentBuffer = new List<int>(); // Buffer for determining which segments are part of one large straight line

    for (int i = 0; i < dirAngles.Count - 1; i++)
    {
        // Preserving skewed walls: Leave walls that are obviously meant to be skewed 45˚+/- tolerance˚ (e.g.angle 30-60 degrees) off main walls as untouched
        if (orgAngles[i] % 90 > (45 - skewTolerance) && orgAngles[i] % 90 < (45 + skewTolerance))
        { continue;
        }
        // Dealing with adjacent segments following the same direction
        segmentBuffer.Add(i);
        if (dirAngles[i] ==
            dirAngles
                [i + 1]) // If next segment is of same orientation, we need 180 deg angle for straight line. Keep checking.
        {
            if (orgAngles[i + 1] % 90 > (45 - skewTolerance) && orgAngles[i + 1] % 90 < (45 + skewTolerance))
            {
                continue;
            }
        }

        if (dirAngles[i] == 0 || dirAngles[i] == 2) // for N,S segments average x coordinate
        {
            float tempX = rotatedX.GetRange(segmentBuffer[0], segmentBuffer.Count).Average();
            // Update with new coordinates
            for (int j = segmentBuffer[0]; j <= segmentBuffer[segmentBuffer.Count - 1]; j++)
            {
                rotatedX[j] = tempX;
            }
        }
        else if (dirAngles[i] == 1 || dirAngles[i] == 3) // for E,W segments average y coordinate
        {
            float tempY = rotatedY.GetRange(segmentBuffer[0], segmentBuffer.Count).Average();
            // Update with new coordinates
            for (int j = segmentBuffer[0]; j <= segmentBuffer[segmentBuffer.Count - 1]; j++)
            {
                rotatedY[j] = tempY;
            }
        }

        if (segmentBuffer.Contains(0)) // Copy change in first point to its last point so we don't lose it during Reverse shift
        {
            rotatedX[rotatedX.Count - 1] = rotatedX[0];
            rotatedY[rotatedY.Count - 1] = rotatedY[0];
        }

        segmentBuffer.Clear();
    }

// Reverse shift so we get polygon with the same start/end point as before
if (shift != 0)
{
rotatedX = rotatedX.Skip(shift).Concat(rotatedX.Take(shift + 1)).ToList();
rotatedY = rotatedY.Skip(shift).Concat(rotatedY.Take(shift + 1)).ToList();
}
else
{
rotatedX[0] = rotatedX[rotatedX.Count - 1]; // Copy updated coordinates to first node
rotatedY[0] = rotatedY[rotatedY.Count - 1];
}

    for (int i = 0; i < rotatedX.Count; i++)
    {
        poly.Add(new Vector2(rotatedX[i], rotatedY[i]));
    }

    return RotatePolygon(poly, -medAngle);
}


/**  :Returns:
  - orgAngle: Segments bearing
  - corAngle: Segments angles to closest cardinal direction
  - dirAngle: Segments direction [N, E, S, W] as [0, 1, 2, 3]*/
public ( List<float>, List<float>, List<int>) CalculateSegmentAngles(List<Vector2> polySimple,
    float maxAngleChange = 45f)
{
    if (polySimple == null || polySimple.Count < 2)
    {
        Debug.LogError("Polygon should contain at least two points.");
        return (null, null, null);
    }

    List<float> orgAngle = new List<float>();
    List<float> corAngle = new List<float>();
    List<int> dirAngle = new List<int>();
    float[] limit = new float[4];

    maxAngleChange = 45 - maxAngleChange;

    for (int i = 0; i < 4; i++)
    {
        limit[i] = 0;
    }

    // Calculate angles between consecutive segments
    for (int i = 0; i < polySimple.Count; i++)
    {
        Vector2 point1 = polySimple[i];
        Vector2 point2 = polySimple[(i + 1) % polySimple.Count];
        float angle = CalculateInitialCompassBearing(point1, point2);

        if (angle > 45 + limit[1] && angle <= 135 - limit[1])
        {
            orgAngle.Add(angle);
            corAngle.Add(angle - 90);
            dirAngle.Add(1);
        }
        else if (angle > 135 + limit[2] && angle <= 225 - limit[2])
        {
            orgAngle.Add(angle);
            corAngle.Add(angle - 180);
            dirAngle.Add(2);
        }
        else if (angle > (225 + limit[3]) && angle <= (315 - limit[3]))
        {
            orgAngle.Add(angle);
            corAngle.Add(angle - 270);
            dirAngle.Add(3);
        }
        else if (angle > (315 + limit[0]) && angle <= 360)
        {
            orgAngle.Add(angle);
            corAngle.Add(angle - 360);
            dirAngle.Add(0);
        }
        else if (angle >= 0 && angle <= (45 - limit[0]))
        {
            orgAngle.Add(angle);
            corAngle.Add(angle);
            dirAngle.Add(0);
        }

        for (int b = 0; b < 4; b++)
        {
            limit[b] = 0;
        }

        limit[dirAngle[i]] = maxAngleChange;
        limit[(dirAngle[i] + 1) % 4] = -maxAngleChange;
        limit[(dirAngle[i] - 1 + 4) % 4] = -maxAngleChange;
    }

    return (orgAngle, corAngle, dirAngle);
}

public List<Vector2> RotatePolygon(List<Vector2> polygon, float angle)
{
    Vector2 centroid = CalculateCentroid(polygon);
    List<Vector2> rotatedPolygon = new List<Vector2>();
    foreach (Vector2 vertex in polygon)
    {
        Vector2 rotatedVertex = RotatePoint(vertex, centroid, angle);
        rotatedPolygon.Add(rotatedVertex);
    }

    return rotatedPolygon;
}

private Vector2 RotatePoint(Vector2 point, Vector2 pivot, float angle)
{
    float angleInRadians = angle * Mathf.Deg2Rad;
    float cosTheta = Mathf.Cos(angleInRadians);
    float sinTheta = Mathf.Sin(angleInRadians);

    float newX = cosTheta * (point.x - pivot.x) - sinTheta * (point.y - pivot.y) + pivot.x;
    float newY = sinTheta * (point.x - pivot.x) + cosTheta * (point.y - pivot.y) + pivot.y;

    return new Vector2(newX, newY);
}

private Vector2 CalculateCentroid(List<Vector2> polygon)
{
    Vector2 centroid = Vector2.zero;
    foreach (Vector2 vertex in polygon)
    {
        centroid += vertex;
    }

    centroid /= polygon.Count;
    return centroid;
}

public float CalculateInitialCompassBearing(Vector2 pointA, Vector2 pointB)
{
    // Check if the inputs are vectors
    if (!(pointA is Vector2) || !(pointB is Vector2))
    {
        throw new ArgumentException("Only vectors are supported as arguments");
    }

    // Convert latitude and longitude from degrees to radians
    float lat1 = Mathf.Deg2Rad * pointA.x;
    float lat2 = Mathf.Deg2Rad * pointB.x;

    float diffLong = Mathf.Deg2Rad * (pointB.y - pointA.y);

    float x = Mathf.Sin(diffLong) * Mathf.Cos(lat2);
    float y = Mathf.Cos(lat1) * Mathf.Sin(lat2) - (Mathf.Sin(lat1) * Mathf.Cos(lat2) * Mathf.Cos(diffLong));

    // Calculate the initial bearing
    float initialBearing = Mathf.Atan2(x, y);


    // Normalize the initial bearing to be between 0 and 360 degrees
    float compassBearing = (Mathf.Rad2Deg * initialBearing + 360) % 360;

    return compassBearing;
}

void DrawBoundingBox(Bounds boundingBox)
{
    Vector3 min = boundingBox.min;
    Vector3 max = boundingBox.max;
    Vector3[] corners = new Vector3[]
    {
        new Vector3(min.x, 0, min.y),
        new Vector3(min.x, 0, max.y),
        new Vector3(max.x, 0, max.y),
        new Vector3(max.x, 0, min.y),
        new Vector3(min.x, 0, min.y)
    };
    for (int i = 0; i < corners.Length - 1; i++)
    {
        Gizmos.color = new Color(1f, 0.69f, 0.16f);
        Gizmos.DrawLine(corners[i], corners[i + 1]);
    }
}

void DrawPolygon(List<Vector2> vertices, Color color)
{
    for (int i = 0; i < vertices.Count; i++)
    {
        Vector2 start = vertices[i];
        Vector2 end = vertices[(i + 1) % vertices.Count];

        Gizmos.color = color;
        Gizmos.DrawLine(new Vector3(start.x, 0, start.y), new Vector3(end.x, 0, end.y));
        Debug.LogError(start);
    }
}

Bounds CalculateBoundingBox(List<Vector2> vertices)
{
    Vector2 min = vertices[0];
    Vector2 max = vertices[0];
    foreach (Vector2 vertex in vertices)
    {
        min = Vector2.Min(min, vertex);
        max = Vector2.Max(max, vertex);
    }

    return new Bounds((min + max) / 2, max - min);
}

public static float Median(IEnumerable<float> source)
{
    var sorted = source.OrderBy(x => x).ToList();
    var count = sorted.Count;
    if (count % 2 == 0)
    {
        return (sorted[count / 2 - 1] + sorted[count / 2]) / 2;
    }
    else
    {
        return sorted[count / 2];
    }
}

public static float StandardDeviation(IEnumerable<float> source)
{
    var values = source.ToList();
    var count = values.Count;
    var mean = values.Average();
    var sumOfSquares = values.Sum(x => Mathf.Pow(x - mean, 2));
    return Mathf.Sqrt(sumOfSquares / count);
}

}`

question about crs

thanks for your job! 😁
i have some question about crs
your code must transfrom 3857 from rotate ,and input polygon is 4326
i tried other crs ,for example input 3957 for whole ,but the result is bad
i want to know why?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.