bad math or bad programming, maybe both?
I'm writing a Python program to generate the Luna Free State flag from the famous Heinlein novel The Moon is a Harsh Mistress, as a personal project. I've been cribbing heraldry rules and matching mathematical formulas off the web, but something is clearly wrong in my bendsinister
routine, since the assertion fails when uncommented. The area of the bend sinister should be 1/3 the total area of the flag, and it isn't. The only really dodgy thing I've done is to guess at the formula for the height of the trapezoid, but I guess the errors could be anywhere. I've trimmed out most of the code, leaving only what's necessary to show the problem. Hopefully someone less mathematically-challenged can spot the error!
#!/usr/bin/python
'generate bend sinister according to rules of heraldry'
import sys, os, random, math, Image, ImageDraw
FLAG = Image.new('RGB', (900, 600), 'black')
CANVAS = ImageDraw.Draw(FLAG)
DEBUGGING = True
def bendsinister(image = FLAG, draw = CANVAS):
'''a bend sinister covers 1/3 of the field, sinister chief to dexter base
(some sources on the web say 1/5 of the field, but we'll use 1/3)
the "field" in this case being the area of the flag, so we need to
find a trapezoi开发者_StackOverflow中文版d which is 1/6 the total area (width * height).
we need to return only the width of the diagonal, which is double
the height of the calculated trapezoid
'''
x, y = image.size
b = math.sqrt((x ** 2) + (y ** 2))
A = float(x * y)
debug('%d * %d = %d' % (x, y, A))
H = triangle_height(A / 2, b) # height of triangular half of flag
width = trapezoid_height(b, H, A / 6) * 2
if command == 'bendsinister':
show_bendsinister(x, y, width, image, draw)
return width
def show_bendsinister(x, y, width, image = FLAG, draw = CANVAS):
'for debugging formula'
dexter_base, sinister_chief = (0, y), (x, 0)
draw.line((dexter_base, sinister_chief), 'blue', int(width))
image.show()
debug(image.getcolors(2)) # should be twice as many black pixels as blue
def triangle_height(a, b):
'a=bh/2'
h = float(a) / (float(b) / 2)
debug('triangle height: %.2f' % h)
return h
def trapezoid_height(b, H, a):
'''calculate trapezoid height (h) given the area (a) of the trapezoid and
base b, the longer base, when it is known that the trapezoid is a section
of a triangle of height H, such that the top, t, equals b when h=0 and
t=0 when h=H. h is therefore inversely proportional to t with the formula
t=(1-(h/H))*b, found simply by looking for what fit the two extremes.
the area of a trapezoid is simply the height times the average length of
the two bases, b and t, i.e.: a=h*((b+t)/2). the formula reduces
then to (2*a)/b=(2*h)+(h**2)/H, which is the quadratic equation
(1/H)*(h**2)+(2*h)-((2*a)/b)=0; solve for h using the quadratic formula
'''
try:
h = (-2 + math.sqrt(4 - 4 * (1.0 / H) * -((2 * a) / b))) / (2 * (1.0 / H))
debug('trapezoid height with plus: %.2f' % h)
except: # must be imaginary, so try minus instead
h = (-2 - math.sqrt(4 - 4 * (1.0 / H) * -((2 * a) / b))) / (2 * (1.0 / H))
debug('trapezoid height with minus: %.2f' % h)
t = (1 - (float(h) / H)) * b
debug('t=%d, a=%d, check=%d' % (t, round(a), round(h * ((b + t) / 2))))
#assert round(a) == round(h * ((b + t) / 2))
return h
def debug(message):
if DEBUGGING:
print >>sys.stderr, message
if __name__ == '__main__':
command = os.path.splitext(os.path.basename(sys.argv[0]))[0]
print eval(command)(*sys.argv[1:]) or ''
Here is the debugging output, showing I'm far off from the 1/3 area:
jcomeau@intrepid:~/rentacoder/jcomeau/tanstaafl$ ./bendsinister.py 900 * 600 = 540000 triangle height: 499.23 trapezoid height with plus: 77.23 t=914, a=90000, check=77077 [(154427, (0, 0, 255)), (385573, (0, 0, 0))] 154.462354191
Here is an image of the output, with some added lines:
The red line divides the two triangles, either can be used for the calculation of the trapezoid. I'm using the one starting at the top left. The green line is the height of that triangle, the variable H in the program.For the finished script and flag (using the correction supplied by Michael Anderson), see http://unternet.net/tanstaafl/. Thanks all for the help!
Break the rectangle into two triangles. They will be identical.
The Black triangle + Blue Trapezoid is Triangle A. The Black Triangle on its own is Triangle B
Triangle A and Triangle B are similar triangles so their area is related by the square of the scale factor relating them.
We want the Blue Trapezoid to be one third of the area of Triangle A. (This way the bend will take one third of the overall rectangle). This means that Triangle B must be 2/3 area of Triangle A. Thus the scalefactor must be sqrt(2/3).
You should then be able to convert this to give you the coordinates of the bend geometry pretty easily.
I executed the following code in an IDLE session
from PIL import Image, ImageDraw
from math import sqrt
'generate bend sinister according to rules of heraldry'
import sys, os, random, math
FLAG = Image.new('RGB', (900, 600), 'black')
CANVAS = ImageDraw.Draw(FLAG)
DEBUGGING = True
def debug(message):
if DEBUGGING:
print >>sys.stderr, message
def show_bendsinister(x, y, width, image = FLAG, draw = CANVAS):
'for debugging formula'
dexter_base, sinister_chief = (0, y), (x, 0)
print 'dexter_base==',dexter_base,'sinister_chief==',sinister_chief
draw.line((dexter_base, sinister_chief), 'blue', int(width))
image.show()
debug(image.getcolors(2)) # should be twice as many black pixels as blue
def trapezoid_height(x, y, P):
'''Given a rectangle whose width and length are (x) and (y)
The half of this rectangle is a large triangle A
whose base (b) is the diagonal of the rectangle
and its height (H) goes from its base (b) to
the right angle of the large triangle.
(x) and (y) are the side-lengths of the triangle.
The area of this large triangle is (x*y)/2 = (H*b)/2
Given a trapezoid whose base is the diagonal (b) of the rectangle
and base (b) of the large triangle, its height is (h)
and its top is (t).
Given (S) as the area of the trapezoid.
In general, the trapezoid is disymtric because the triangle have x != y.
So the area is S = h*(b + t)/2
This function trapezoid_height() calculates the height (h) of the trapezoid
in order that the trapezoid have an area (S) which must be
the percentage (P) of the area of the large triangle A. So:
h*(b + t)/2 = S = P*[H*b /2] ==> h*(b + t) = P*H*b
==> h*t = P*H*b - h*b ==> h*t*(H-h) = [P*H - h]*b*(H-h)
The large triangle is the sum of the trapezoid and of a little triangle B
having an height equal to (H-h) and a base which is the top (t)
of the trapezoid.
The area of this little triangle B is t*(H-h)/2 and must be equal to (1-P)*[H*b / 2]
==> t*(H-h) = (1-P)*H*b ==> h*t*(H-h) = h*(1-P)*H*b
From h*t*(H-h) = [P*H - h]*b*(H-h) and h*t*(H-h) = h*(1-P)*H*b
we obtain [P*H - h]*b*(H-h) = h*(1-P)*H*b
==> b*h**2 - (b*H + xy)*h + P*x*y*H = 0
==> h**2 - 2*H*h + P*(H**2) = 0
That leads to the solution H*(1 - sqrt(1-P)), the other H*(1 + sqrt(1-P))
being bigger than H
'''
H = math.sqrt( (x*x*y*y) / (x*x + y*y) )
return H*(1 - sqrt(1-P))
def bendsinister(image = FLAG, draw = CANVAS):
'''a bend sinister covers 1/3 of the field, sinister chief to dexter base
(some sources on the web say 1/5 of the field, but we'll use 1/3)
the "field" in this case being the area of the flag, so we need to
find a trapezoid which is 1/6 the total area (width * height).
we need to return only the width of the diagonal, which is double
the height of the calculated trapezoid
'''
x, y = image.size
print 'x ==',x,'y ==',y
percentage = float(1)/3
width = 2 * trapezoid_height(x, y , percentage)
print 'height ==',width/2
print 'width==',width
if command == 'bendsinister':
show_bendsinister(x, y, width, image, draw)
return width
command = 'bendsinister'
print bendsinister()
result
x == 900 y == 600
height == 91.6103029364
width== 183.220605873
dexter_base== (0, 600) sinister_chief== (900, 0)
[(180340, (0, 0, 255)), (359660, (0, 0, 0))]
183.220605873
The blue stripe displayed doesn't give the impression to be 1/3 of the field's area, but the numbers speak:
359660 / 180340 = 1.994344
精彩评论