Added a whole bunch of variations on Bresenham's algorithm and a few tests.

The tests check if any of the variations change the algorithm's output to see if I broke anything while making the 3D and fixed-point/scaled variants.
This commit is contained in:
David Vierra 2015-05-12 23:27:23 -10:00
parent 01e08a2d9c
commit fe64ea61a5

View File

@ -1,44 +1,333 @@
from __future__ import division
import collections
import functools
from math import floor
# Original form of bresenham's plotting algorithm:
import itertools
def bresenham_0((x1, y1), (x2, y2)):
e = 0 # error term
y = y1 # current y
m = (y2 - y1) / (x2 - x1) # slope
for x in range(x1, x2):
yield x, y
if e + m < 0.5:
e += m
else:
y += 1
e += m - 1
# ---
# Hoist the += m out of the if
def bresenham_1((x1, y1), (x2, y2)):
e = 0
y = y1
m = (y2 - y1) / (x2 - x1)
assert -1 <= m <= 1
for x in range(x1, x2):
yield x, y
e += m
if e >= 0.5:
y += 1
e -= 1
# ---
# Multiply e and m by 2 * dx. m decomposes into dx and dy
# Gains a bit of accuracy by losing the float division for m.
def bresenham_2((x1, y1), (x2, y2)):
e = 0
y = y1
dy = y2 - y1
dx = x2 - x1
# m = (y2-y1)/(x2-x1)
assert dx > dy
for x in range(x1, x2):
yield x, y
e += 2 * dy
if e >= dx:
y += 1
e -= 2 * dx
# ---
# Initialize e to -dx
def bresenham_3((x1, y1), (x2, y2)):
y = y1
dx = x2 - x1
dy = y2 - y1
e = -dx
assert dx > dy
for x in range(x1, x2):
yield x, y
e += 2 * dy
if e >= 0:
y += 1
e -= 2 * dx
# ---
# Convert to 3D coordinates:
# Add z1 and z2
# Use multiple 'e' values for y and z
def bresenham3D_3((x1, y1, z1), (x2, y2, z2)):
y = y1
z = z1
dx = x2 - x1
dy = y2 - y1
dz = z2 - z1
ey = -dx
ez = -dx
assert dx > dy and dx > dz
for x in range(x1, x2):
yield x, y, z
ey += 2 * dy
if ey >= 0:
y += 1
ey -= 2 * dx
ez += 2 * dz
if ez >= 0:
z += 1
ez -= 2 * dx
# ---
# from bresenham_3:
# Scale up to allow float coordinates as input
# Multiply x1, x2, y1, y2 by 65536
# Multiply step by 65536
# Divide yielded output by 65536
def bresenham_4((x1, y1), (x2, y2)):
step = 65536
x1 = int(floor(x1 * step))
x2 = int(floor(x2 * step))
y1 = int(floor(y1 * step))
y2 = int(floor(y2 * step))
y = y1
dx = x2 - x1
dy = y2 - y1
e = -dx
assert dx > dy
for x in range(x1, x2, step):
yield x >> 16, y >> 16
e += 2 * dy
if e >= 0:
y += step
e -= 2 * dx
# ---
# Convert to 3D coordinates:
# Add z1 and z2
# Use multiple 'e' values for y and z
def bresenham3D_4((x1, y1, z1), (x2, y2, z2)):
step = 65536
x1 = int(floor(x1 * step))
x2 = int(floor(x2 * step))
y1 = int(floor(y1 * step))
y2 = int(floor(y2 * step))
z1 = int(floor(z1 * step))
z2 = int(floor(z2 * step))
y = y1
z = z1
dx = x2 - x1
dy = y2 - y1
dz = z2 - z1
ey = -dx
ez = -dx
assert dx > dy and dx > dz
for x in range(x1, x2, step):
yield x >> 16, y >> 16, z >> 16
ey += 2 * dy
if ey >= 0:
y += step
ey -= 2 * dx
ez += 2 * dz
if ez >= 0:
z += step
ez -= 2 * dx
# ---
# Relax requirement that x is the major axis and dx, dy, dz are positive
# Store x, y, z in a list instead of directly as variables
# Store ex, ey, ez in a list. One of them is ignored (and pointlessly updated)
# Compute an index into this list for the major axis
# Make step negative whenever d is negative
def bresenham(p1, p2):
"""
Bresenham line algorithm
adapted for 3d. slooooow.
Now in generator style.
Bresenham line algorithm adapted for 3d and scaled for subpixel endpoints.
"""
x, y, z = p1
step = 65536
logStep = 16
# step = 1
# logStep = 0
x1, y1, z1 = p1
x2, y2, z2 = p2
dx = abs(x2 - x)
if (x2 - x) > 0:
sx = 1
else:
sx = -1
dy = abs(y2 - y)
if (y2 - y) > 0:
sy = 1
else:
sy = -1
dz = abs(z2 - z)
if (z2 - z) > 0:
sz = 1
else:
sz = -1
x1 = int(floor(x1 * step))
y1 = int(floor(y1 * step))
z1 = int(floor(z1 * step))
x2 = int(floor(x2 * step))
y2 = int(floor(y2 * step))
z2 = int(floor(z2 * step))
dl = [dx, dy, dz]
longestAxis = dl.index(max(dl))
d = [2 * a - dl[longestAxis] for a in dl]
dx = abs(x2 - x1)
if (x2 - x1) > 0:
sx = step
else:
sx = -step
dy = abs(y2 - y1)
if (y2 - y1) > 0:
sy = step
else:
sy = -step
dz = abs(z2 - z1)
if (z2 - z1) > 0:
sz = step
else:
sz = -step
# absolute value of distance vector (dy)
distance = [dx, dy, dz]
# index of axis corresponding to dx
longestAxis = distance.index(max(distance))
# accumulated error along minor axes, multiplied by 2*dx
# initialized to -dx so the test becomes error >= 0
error = [-distance[longestAxis] for _ in distance]
otherAxes = [0, 1, 2]
otherAxes.remove(longestAxis)
p = [x, y, z]
sp = [sx, sy, sz]
for i in range(0, int(dl[longestAxis])):
yield tuple(p)
point = [x1, y1, z1]
steps = [sx, sy, sz]
for i in range(0, int(distance[longestAxis]), step):
yield tuple([(a >> logStep) for a in point])
# add dy to error terms
error = map(lambda e, d: e + 2 * d, error, distance)
for j in otherAxes:
while error[j] >= 0:
point[j] += steps[j]
error[j] -= 2 * distance[longestAxis]
while d[j] >= 0:
p[j] += sp[j]
d[j] -= 2 * dl[longestAxis]
# step along major axis
point[longestAxis] += steps[longestAxis]
p[longestAxis] += sp[longestAxis]
d = map(lambda a, b: a + 2 * b, d, dl)
def testInt2D():
p1 = -10, -5
p2 = 2, 6
r0 = list(bresenham_0(p1, p2))
r1 = list(bresenham_1(p1, p2))
r2 = list(bresenham_2(p1, p2))
r3 = list(bresenham_3(p1, p2))
r4 = list(bresenham_4(p1, p2))
if r0 != r1: # no change
print "r0 != r1\n%s != \n%s" % (r0, r1)
if r1 == r2: # accuracy gained
print "r1 == r2\n%s != \n%s" % (r1, r2)
if r2 != r3: # no change
print "r2 != r3\n%s != \n%s" % (r2, r3)
if r3 != r4: # no change
print "r3 != r4\n%s != \n%s" % (r3, r4)
def testFloat2D():
p1 = -64, -30
p2 = -2.66773328, -6.3785856262
i1 = -64, -30
i2 = -3, -7
r3 = list(bresenham_3(i1, i2))
r4 = list(bresenham_4(p1, p2))
if r3 == r4: # accuracy gained
print "r3 == r4\n%s != \n%s" % (r3, r4)
def testFloat2D3D():
testFloat2D3DWith(bresenham_3, bresenham3D_3, True)
testFloat2D3DWith(bresenham_4, bresenham3D_4)
def testFloat2D3DWith(bres2d, bres3d, asInt=False):
# 3D variant should project exactly onto 2D variant along the two non-major axes.
# It will not project along the major axis because it will sometimes move along one minor axis and then the other,
# which will draw a right-angle which is never normally drawn.
print "Trying", bres2d, bres3d
p1 = -64, -30, -15
p2 = -2.66773328, -6.3785856262, 4.3333
if asInt:
p1 = map(int, p1)
p2 = map(int, p2)
r4_xy = list(bres2d(p1[:2], p2[:2]))
r4_xz = list(bres2d((p1[0], p1[2]), (p2[0], p2[2])))
r5 = list(bres3d(p1, p2))
r5_xy = [(x, y) for x, y, z in r5]
r5_xz = [(x, z) for x, y, z in r5]
if r4_xy != r5_xy: # no change for (x, y)
print "2d_xy != 3d_xy\n%s != \n%s" % (r4_xy, r5_xy)
if r4_xz != r5_xz: # no change for (x, z)
print "2d_xz != 3d_xz\n%s != \n%s" % (r4_xz, r5_xz)
def testFloat3D():
p1 = -64, -30, -15
p2 = -2.66773328, -6.3785856262, 4.3333
r0 = list(bresenham3D_4(p1, p2))
r1 = list(bresenham(p1, p2))
if r0 != r1: # no change
print "r0 != r1\n%s != \n%s" % (r0, r1)
def main():
testInt2D()
testFloat2D()
testFloat2D3D()
testFloat3D()
if __name__ == '__main__':
main()