Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

algorithm for undoing Bresenham lines

I have a blob of points on a grid that I want to create a sensible polygon outline for. The points will be selected by the user so I can’t expect them to be perfectly following Bresenham’s algorithm for lines with weird slopes. However, I am still struggling to even get something working for an obvious "nice" sloped side:

#
###
#####
#######
#####
###
#

What I want is to turn those points into an SVG polygon (or path, or polyline, etc). It is supposed to be a nice neat triangle as you might expect.

Here is the code I have tried so far:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

import cmath

s = """
#
###
#####
#######
#####
###
#
"""
pts = [complex(c, r)
       for (r, rt) in enumerate(s.splitlines())
       for (c, ch) in enumerate(rt)
       if ch == "#"]


def centroid(pts: list[complex]) -> complex:
    return sum(pts) / len(pts)


def sort_counterclockwise(pts: list[complex],
                          center: complex | None = None) -> list[complex]:
    if center is None:
        center = centroid(pts)
    return sorted(pts, key=lambda p: cmath.phase(p - center))


def perimeter(pts: list[complex]) -> list[complex]:
    out = []
    for pt in pts:
        for d in (-1, 1, -1j, 1j, -1+1j, 1+1j, -1-1j, 1-1j):
            xp = pt + d
            if xp not in pts:
                out.append(pt)
                break
    return sort_counterclockwise(out, centroid(pts))


def example(all_points: list[complex], scale: float = 20) -> str:
    p = perimeter(all_points)
    p.append(p[0])
    vbx = max(map(lambda x: x.real, p)) + 1
    vby = max(map(lambda x: x.imag, p)) + 1
    return f"""<svg
    viewBox="-1 -1 {vbx} {vby}"
    width="{vbx * scale}"
    height="{vbx * scale}">
    <polyline
    fill="none"
    stroke="black"
    stroke-width="0.1"
    points="{" ".join(map(lambda x: f"{x.real},{x.imag}", p))}">
    </polyline></svg>"""


print(example(pts))

It results in a horrible jagged mess:

<svg
    viewBox="-1 -1 7.0 8.0"
    width="140.0"
    height="140.0">
    <polyline
    fill="none"
    stroke="black"
    stroke-width="0.1"
    points="0.0,3.0 0.0,2.0 0.0,1.0 1.0,2.0 2.0,2.0 2.0,3.0 3.0,3.0 4.0,3.0 4.0,4.0 5.0,4.0 6.0,4.0 4.0,5.0 3.0,5.0 2.0,5.0 2.0,6.0 1.0,6.0 0.0,7.0 0.0,6.0 0.0,5.0 0.0,4.0 0.0,3.0">
    </polyline></svg>

Any tips on making the algorithm respond better to making clearly-defined slopes and only produce a triangle for this?

>Solution :

A more appropriate methodology for solving this issue is instead of finding all edge points (any point with an empty neighbour) which is what you are doing. You can instead just find the left most and rightmost point of each row as these are the ones you wish to plot.

We can keep points (pts) defined the same way.

Then we can create an outline like this:

def get_outline_points(pts: List[complex]) -> List[complex]:

    by_y = defaultdict(list)
    for p in pts:
        by_y[p.imag].append(p.real)
    
    # Get leftmost points sorted from bottom to top
    left_side = [
        complex(min(row), y) 
        for y, row in sorted(by_y.items())
    ]
    
    # Get rightmost points sorted from top to bottom
    right_side = [
        complex(max(row), y) 
        for y, row in sorted(by_y.items(), reverse=True)
    ]
    
    # Combine left and right sides to form the outline
    outline = left_side + right_side
    
    return outline

Now you have an outline with straight/ smooth edges.

Finish rendering as svg similarly to how you already do but accounting for outline:

def create_svg(pts: list[complex], scale: float = 20) -> str:
    outline = get_outline_points(pts)
    
    # Close the polygon by adding first point again
    if outline and outline[0] != outline[-1]:
        outline.append(outline[0])
    
    vbx = max(p.real for p in outline) + 1
    vby = max(p.imag for p in outline) + 1
    
    points_str = " ".join(f"{p.real},{p.imag}" for p in outline)
    
    return f"""<svg
    viewBox="-1 -1 {vbx} {vby}"
    width="{vbx * scale}"
    height="{vby * scale}">
    <polygon
        fill="none"
        stroke="black"
        stroke-width="0.1"
        points="{points_str}"
    />
    </svg>"""
#
###
#####
#######
#####
###
#
  <svg
      viewBox="-1 -1 7.0 8.0"
      width="140"
      height="160">
      <polygon
          fill="none"
          stroke="black"
          stroke-width="0.1"
          points="0.0,1.0 0.0,1.0 0.0,2.0 0.0,3.0 0.0,4.0 0.0,5.0 0.0,6.0 0.0,7.0 0.0,7.0 6.0,4.0 0.0,1.0"
      />
  </svg>
#
###
#####
##########
#####
###
#
<svg
    viewBox="-1 -1 10.0 7.0"
    width="200.0"
    height="140.0">
    <polygon
        fill="none"
        stroke="black"
        stroke-width="0.1"
        points="0.0,0.0 0.0,1.0 0.0,2.0 0.0,3.0 0.0,4.0 0.0,5.0 0.0,6.0 2.0,5.0 4.0,4.0 9.0,3.0 4.0,2.0 2.0,1.0 0.0,0.0"
    />
    </svg>
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading