Perlin Noise understanding - python-3.x

I found two tutorials that explains how the Perlin Noise works, but in the first tutorial I found not understandable mystery of gradients, and in the second one I found the mystery of surflets.
First case
The first tutorial is located here catlikecoding.com/unity/tutorials/noise. At first autor explains the value noise which is completely understandable, because all we need to do is to draw a grid of random colors and then just interpolate between the colors.
But when it comes to the Perlin Noise, we have to deal with gradients, not with single colors. At first I thought about gradiens as colors, so if we have two gradients and we want to make interpolation between them, we have to take a respective point of the first gradient and interpolate it with respective point of the second gradient. But if the gradients are the same, we will have a result which is the same as gradients.
In the tutorial author makes it in another way. If we have a 1d grid which consists of columns that are filled with the same gradient, and each gradient can be represented as transition from 0 to 1 (here 0 is black color, and 1 is white color). Then author says
Now every stripe has the same gradient, except that they are offset
from one another. So for every t0, the gradient to the right of it is
t1 = t0 - 1. Let's smoothly interpolate them.
So it means that we have to interpolate between a gradient which is represented as transition from 0 to 1, and a gradient which is represented as transition from -1 to 0.
It implies that every gradient doesn't start at position with value of 0 and doesn't stop at position with value of 1. It starts somewhere at -1 and ends somewhere at 2, or maybe it has no start and end points. We can see only 0 to 1 range, and I can't understand why it is like this. Whence did we take the idea of continuous gradient? I thought that we have only gradient from 0 to 1 for every strip and that's all, don't we?
When I asked the author about all this he answered like this is something obvious
The gradient to the right is a visual reference. It’s the gradient for
the next higher integer. You’re right that it goes negative to the
left. They all do.
So t0 is the gradient that’s zero at the lattice point on the left
side of the region between two integers. And t1 is the gradient that’s
zero at the lattice point on the right side of the same region.
Gradient noise is obtained by interpolating between these two
gradients in between lattice points. And yes, that can produce
negative results, which end up black. That’s why the next step is to
scale and offset the result.
Now I feel like this is impossible for me to understand how this works, so I have just to believe and repeat after smarter guys. But hope dies last, so I beg you to explain it to me somehow.
Second case
The second tutorial is located here eastfarthing.com/blog/2015-04-21-noise/ and it's much less sophisticated than the previous one.
The only problem I had encountered is that I can't understand next paragraph and what's going on after this
So given this, we can just focus on the direction of G and always use
unit length vectors. If we clamp the product of the falloff kernel and
the gradient to 0 at all points beyond the 2×2 square, this gives us
the surflet mentioned in that cryptic sentence.
I'm not sure whether the problem is in my poor math or English knowledge, so I ask you to explain what does this actually mean in simple words.
Here is some code I have written so far, it relates to the second case
import sys
import random
import math
from PyQt4.QtGui import *
from PyQt4.QtCore import pyqtSlot
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
self.gx=1
self.gy=0
self.lbl=QLabel()
self.tlb = None
self.image = QImage(512, 512, QImage.Format_RGB32)
self.hbox = QHBoxLayout()
self.pixmap = QPixmap()
self.length = 1
self.initUI()
def mousePressEvent(self, QMouseEvent):
px = QMouseEvent.pos().x()
py = QMouseEvent.pos().y()
size = self.frameSize()
self.gx = px-size.width()/2
self.gy = py-size.height()/2
h = (self.gx**2+self.gy**2)**0.5
self.gx/=h
self.gy/=h
self.fillImage()
def wheelEvent(self,event):
self.length+=(event.delta()*0.001)
print(self.length)
def initUI(self):
self.hbox = QHBoxLayout(self)
self.pixmap = QPixmap()
self.move(300, 200)
self.setWindowTitle('Red Rock')
self.addedWidget = None
self.fillImage()
self.setLayout(self.hbox)
self.show()
def fillImage(self):
step = 128
for x in range(0, 512, step):
for y in range(0, 512, step):
rn = random.randrange(0, 360)
self.gx = math.cos(math.radians(rn))
self.gy = math.sin(math.radians(rn))
for x1 in range(0, step):
t = -1+(x1/step)*2
color = (1 - (3 - 2*abs(t))*t**2)
for y1 in range(0, step):
t1 = -1+(y1/step)*2
color1 = (1 - (3 - 2*abs(t1))*t1**2)
result = (255/2)+(color * color1 * (t*self.gx+t1*self.gy) )*(255/2)
self.image.setPixel(x+x1, y+y1, qRgb(result, result, result))
self.pixmap = self.pixmap.fromImage(self.image)
if self.lbl == None:
self.lbl = QLabel(self)
else:
self.lbl.setPixmap(self.pixmap)
if self.addedWidget == None:
self.hbox.addWidget(self.lbl)
self.addedWidget = True
self.repaint()
self.update()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

float Noise::perlin1D(glm::vec3 point, float frequency)
{
// map the point to the frequency space
point *= frequency;
// get the base integer the point exists on
int i0 = static_cast<int>(floorf(point.x));
// distance from the left integer to the point
float t0 = point.x - static_cast<float>(i0);
// distance from the right integer to the point
float t1 = t0 - 1.f;
// make sure the base integer is in the range of the hash function
i0 &= hashMask;
// get the right integer (already in range of the hash function)
int i1 = i0 + 1;
// choose a pseudorandom gradient for the left and the right integers
float g0 = gradients1D[hash[i0] & gradientsMask1D];
float g1 = gradients1D[hash[i1] & gradientsMask1D];
// take the dot product between our gradients and our distances to
// get the influence values. (gradients are more influential the closer they are to the point)
float v0 = g0 * t0;
float v1 = g1 * t1;
// map the point to a smooth curve with first and second derivatives of 0
float t = smooth(t0);
// interpolate our influence values along the smooth curve
return glm::mix(v0, v1, t) * 2.f;
}
here is a commented version of the code in question. But rewritten for c++. Obviously all credit goes to catlikecoding.
We've given the function a point p. Let's assume the point p is fractional so for example if p is .25 then the integer to the left of p is 0 and the integer to the right of p is 1. Let's call these integers l and r respectively.
Then t0 is the distance from l to p and t1 is the distance from r to p. The distance is negative for t1 since you have to move in a negative direction to get from r to p.
If we continue on to the perlin noise part of this implementation g0 and g1 are pseudorandom gradients in 1 dimension. Once again gradient may be confusing here since g0 and g1 are floats, but a gradient is simply a direction and in one dimension you can only go positive or negative, so these gradients are +1 and -1. We take the dot product between the gradients and the distances, but in one dimension this is simply multiplication. The result of the dot product is the two floats v0 and v1. These are the influence values of our perlin noise implementation. Finally we smoothly interpolate between these influence values to produce a smooth noise function. Let me know if this helps! This perlin noise explanation was very helpful in my understanding of this code.

Related

PyQt creating color circle

I want to add a color circle to the widget placeholder there:
I already tried this library:
https://gist.github.com/tobi08151405/7b0a8151c9df1a41a87c1559dac1243a
But if the window wasnt a quadrat, the color circle didnt worked.
I have already tried the other solution, but there is no method to get the color value.
How to create a "Color Circle" in PyQt?
Could you recommend/ show me a way of creating my own, so I can add them there?
Thanks!
The widget assumes that its shape is always a square; the code provides a custom AspectLayout for that, but it's not necessary.
The problem comes from the fact that when the shape is not a square the computation of the color is wrong, as coordinates are not properly mapped when a dimension is much bigger than the other. For instance, if the widget is much wider than tall, the x coordinate is "shifted" since the circle (which is now an actual ellipse) is shown centered, but the color function uses the minimum size.
The solution is to create an internal QRect that is always displayed at the center and use that for both painting and computation:
class ColorCircle(QWidget):
# ...
def resizeEvent(self, ev: QResizeEvent) -> None:
size = min(self.width(), self.height())
self.radius = size / 2
self.square = QRect(0, 0, size, size)
self.square.moveCenter(self.rect().center())
def paintEvent(self, ev: QPaintEvent) -> None:
# ...
p.setPen(Qt.transparent)
p.setBrush(hsv_grad)
p.drawEllipse(self.square)
p.setBrush(val_grad)
p.drawEllipse(self.square)
# ...
def map_color(self, x: int, y: int) -> QColor:
x -= self.square.x()
y -= self.square.y()
# ...
Note that the code uses numpy, but it's used for functions that do not really require such a huge library for an application that clearly doesn't need numpy's performance.
For instance, the line_circle_inter uses a complex way to compute the position for the "cursor" (the small circle), but that's absolutely unnecessary, as the Hue and Saturation values already provide "usable" coordinates: the Hue indicates the angle in the circle (starting from 12 hour position, counter-clockwise), while the Saturation is the distance from the center.
QLineF provides a convenience function, fromPolar(), which returns a line with a given length and angle: the length will be the radius multiplied by the Saturation, the angle is the Hue multiplied by 360 (plus 90°, as angles start always at 3 o'clock); then we can translate that line at the center of the circle and the cursor will be positioned at the second point of the segment:
def paintEvent(self, event):
# ...
p.setPen(Qt.black)
p.setBrush(self.selected_color)
line = QLineF.fromPolar(self.radius * self.s, 360 * self.h + 90)
line.translate(self.square.center())
p.drawEllipse(line.p2(), 10, 10)
The map color function can use the same logic, but inverted: we construct a line starting from the center to the mouse cursor position, then the Saturation is the length divided by the radius (sanitizing the value to 1.0, as that's the maximum possible value), while the Hue is the line angle (minus 90° as above) divided by 360 and sanitized for a positive 0.0-1.0 range.
def map_color(self, x: int, y: int) -> QColor:
line = QLineF(QPointF(self.rect().center()), QPointF(x, y))
s = min(1.0, line.length() / self.radius)
h = (line.angle() - 90) / 360 % 1.
return h, s, self.v

ipycanvas displaying final stroke_lines thoughout animation

So I was playing with animating some Bezier curves - just part of learning how to use ipycanvas (0,10,2) -- The animation I produced is really hurting my head. What I expected to see was a set of straight lines between 4 Bezier control points "bouncing" around the canvas with the Bezier curve moving along with them.
I did get the moving Bezier curve -- BUT the control points stayed static. Even stranger they were static in the final position and the curve came to meet them.
Now sometimes Python's structures and references can get a little tricky and so you can sometimes get confusing results if you are not really thinking it through -- and this totally could be what's going on - but I am at a loss.
So to make sure I was not confused I printed the control points (pts) at the beginning and then displayed them to the canvas. This confirmed my suspicion. Through quantum tunneling or some other magic time travel the line canvas.stroke_lines(pts) reaches into the future and grabs the pts array as it will exist in the future and keeps the control points in their final state.
Every other use of pts uses the current temporal state.
So what I need to know is A) The laws of physics are safe and I am just too dumb to understand my own code. B) There is some odd bug in ipycanvas that I should report. C) How to monetize this time-traveling code -- like, could we use it to somehow factor large numbers?
from ipycanvas import Canvas, hold_canvas
import numpy as np
def rgb_to_hex(rgb):
if len(rgb) == 3:
return '#%02x%02x%02x' % rgb
elif len(rgb) == 4:
return '#%02x%02x%02x%02x' % rgb
def Bezier4(t, pts):
p = t**np.arange(0, 4,1)
M=np.matrix([[0,0,0,1],[0,0,3,-3],[0,3,-6,3],[1,-3,3,-1]])
return np.asarray((p*M*pts))
canvas = Canvas(width=800, height=800)
display(canvas) # display the canvas in the output cell..
pts = np.random.randint(50, 750, size=[4, 2]) #choose random starting point
print(pts) #print so we can compare with ending state
d = np.random.uniform(-4,4,size=[4,2]) #some random velocity vectors
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3))) #some random color
canvas.font = '16px serif' #font for displaying the changing pts array
with hold_canvas(canvas):
for ani in range(300):
#logic to bounce the points about...
for n in range(0,len(pts)):
pts[n]=pts[n] + d[n]
if pts[n][0] >= 800 or pts[n][0] <= 0 :
d[n][0] = - d[n][0]
if pts[n][1] >= 800 or pts[n][1] <= 0 :
d[n][1] = - d[n][1]
#calculate the points needed to display a bezier curve
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,15)]
#begin display output....
canvas.clear()
#first draw bezier curve...
canvas.stroke_style = c
canvas.stroke_lines(B)
#Now draw control points
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
canvas.stroke_lines(pts)
#print the control points to the canvas so we can see them move
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.stroke_text(str(pts), 10, 32)
canvas.sleep(20)
In all seriousness, I have tried to think through what can be happening and I am coming up blank. Since ipycanvas is talking to the browser/javascript maybe all of the data for the frames are rendered first and the array used to hold the pts data for the stroke_lines ends up with the final values... Whereas the B array is recreated in each loop... It's a guess.
There are two ways to get the code to behave as expected and avoid the unsightly time-traveling code. The first way is to switch the location of the line with hold_canvas(canvas): to inside the loop. This however renders the canvas.sleep(20) line rather useless.
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
#with hold_canvas(canvas):
for ani in range(300):
with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0 :
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50 :
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,25)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
#pts2 = np.copy(pts)
canvas.stroke_lines(pts)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
sleep(20/1000)
#canvas.sleep(20)
In this version, the control lines are updated as expected. This version is a little more "real time" and thus the sleep(20/1000) is needed to
The other way to do it would be just to ensure that a copy of pts is made and passed to canvas.stroke_lines:
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
with hold_canvas(canvas):
for ani in range(300):
#with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0:
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50:
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,35)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
pts2 = np.copy(pts)
canvas.stroke_lines(pts2)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
#sleep(20/1000)
canvas.sleep(20)
I could not actually find the data passed between the python and the browser but it seems pretty logical that what is happening is that python is finishing its work (and ani loop) before sending the widget instructions on what to draw, and the pts values sent are the final ones.
(yes I know there is a bug in the bouncing logic)

How to get cursor coordinates relative to matrix scale in pyglet/opengl?

I am making a 2D game in pyglet and use both glTranslatef and glScalef:
def background_motion(dt):
if stars.left:
pyglet.gl.glTranslatef(stars.speed, 0, 0)
stars.translation[0] += stars.speed
if stars.right:
pyglet.gl.glTranslatef(-stars.speed, 0, 0)
stars.translation[0] -= stars.speed
if stars.up:
pyglet.gl.glTranslatef(0, -stars.speed, 0)
stars.translation[1] -= stars.speed
if stars.down:
pyglet.gl.glTranslatef(0, stars.speed, 0)
stars.translation[1] += stars.speed
pyglet.clock.schedule_interval(background_motion, 0.05)
#window.event
def on_mouse_scroll(x, y, scroll_x, scroll_y):
if scroll_y > 0:
stars.scale += 0.01
elif scroll_y < 0:
stars.scale -= 0.01
#window.event
def on_draw():
window.clear()
pyglet.gl.glScalef(stars.scale,stars.scale, 1, 1)
stars.image.draw()
for s in game.ships:
s.draw()
pyglet.gl.glPushMatrix()
pyglet.gl.glLoadIdentity()
#HUD Start
overlay.draw(stars.image.x,stars.image.y,game.ships,stars.scale,stars.image.width)
if game.pause:
pause_text.draw()
#HUD End
pyglet.gl.glPopMatrix()
stars.scale = 1
However I also need the cursor coordinates relative to the background. For the movement I simply added the translation onto the x y coordinates which works however only when I don't scale the matrix:
#window.event
def on_mouse_motion(x, y, dx, dy):
if player.course_setting:
player.projected_heading = (x - stars.translation[0],y -stars.translation[1])
How can I get the cursor coordinates accounting for scale?
You'll have to unproject the pointer position. Projection happens as following:
p_eye = M · p
p_clip = P · p_eye
at this point the primitive is clipped, but we can ignore this for the moment. After clipping comes the homogenous divide, which brings the coordinates into NDC space, i.e. the viewport is treated as a cuboid of dimensions [-1,1]×[-1,1]×[0,1]
p_NDC = p_clip / p_clip.w
From there it's mapped into pixel dimensions. I'm going to omit this step here.
Unprojecting is doing these operations in reverse. There's a small trick in there, regarding the homogenous divide, though; this is kind of an "antisymmetric" (not the proper term for this, but it gets across the point) operation, and happens at the end, for each projection and unprojection. Unprojection hence is
p_NDC.w = 1
p_eye' = inv(P)·p_NDC
p' = inv(M)·p_eye'
p = p' / p'.w
All of this has been wrapped into unproject functions for your convenience by GLU (if you insist on using the fixed function matrix stack) or GLM – but not my linmath.h, though.

ValueError: operands could not be broadcast together with shapes (3,) (0,)

My aim is to make the image1 move along the ring from its current position upto 180 degree. I have been trying to do different things but nothing seem to work. My final aim is to move both the images along the ring in different directions and finally merge them to and make them disappear.I keep getting the error above.Can you please help? Also can you tell how I can go about this problem?
from visual import *
import numpy as np
x = 3
y = 0
z = 0
i = pi/3
c = 0.120239 # A.U/minute
r = 1
for theta in arange(0, 2*pi, 0.1): #range of theta values; 0 to
xunit = r * sin(theta)*cos(i) +x
yunit = r * sin(theta)*sin(i) +y
zunit = r*cos(theta) +z
ring = curve( color = color.white ) #creates a curve
for theta in arange(0, 2*pi, 0.01):
ring.append( pos=(sin(theta)*cos(i) +x,sin(theta)*sin(i) +y,cos(theta) +z) )
image1=sphere(pos=(2.5,-0.866,0),radius=0.02, color=color.yellow)
image2=sphere(pos=(2.5,-0.866,0),radius=0.02, color=color.yellow)
earth=sphere(pos=(-3,0,-0.4),color=color.yellow, radius =0.3,material=materials.earth) #creates the observer
d_c_p = pow((x-xunit)**2 + (y-yunit)**2 + (z-zunit)**2,0.5) #calculates the distance between the center and points on ring
d_n_p = abs(yunit + 0.4998112152755791) #calculates the distance to the nearest point
t1 = ( d_c_p+d_n_p)/c
t0=d_c_p/c
t=t1-t0 #calculates the time it takes from one point to another
theta = []
t = []
dtheta = np.diff(theta) #calculates the difference in theta
dt = np.diff(t) #calculates the difference in t
speed = r*dtheta/dt #hence this calculates the speed
deltat = 0.005
t2=0
while True:
rate(5)
image2.pos = image2.pos + speed*deltat #increments the position of the image1
t2 = t2 + deltat
Your problem is that image2.pos is a vector (that's the "3" in the error message) but speed*deltat is a scalar (that's the "0" in the error message). You can't add a vector and a scalar. Instead of a scalar "speed" you need a vector velocity. There seem to be some errors in indentation in the program you posted, so there is some possibility I've misinterpreted what you're trying to do.
For VPython questions it's better to post to the VPython forum, where there are many more VPython users who will see your question than if you post to stackoverflow:
https://groups.google.com/forum/?fromgroups&hl=en#!forum/vpython-users

bezier path widening

I have a bezier curve B with points S, C1, C2, E, and a positive number w representing width. Is there a way of quickly computing the control points of two bezier curves B1, B2 such that the stuff between B1 and B2 is the widened path represented by B?
More formally: compute the control points of good Bezier approximations to B1, B2, where
B1 = {(x,y) + N(x,y)(w/2) | (x,y) in C}
B2 = {(x,y) - N(x,y)(w/2) | (x,y) in C},
where N(x,y) is the normal
of C at (x,y).
I say good approximations because B1, B2 might not be polynomial curves (I'm not sure if they are).
The exact parallel of a bezier curve is quite ugly from a mathematical point of view (it requires 10th-degree polynomials).
What is easy to do is compute a widening from a polygonal approximation of the bezier (that is you compute line segments from the bezier and then move the points along the normals on the two sides of the curve).
This gives good results if your thickness isn't too big compared to the curvature... a "far parallel" instead is a monster on its own (and it's not even easy to find a definition of what is a parallel of an open curve that would make everyone happy).
Once you have two polylines for the two sides what you can do is finding a best approximating bezier for those paths if you need that representation. Once again I think that for "normal cases" (that is reasonably thin lines) even just a single bezier arc for each of the two sides should be quite accurate (the error should be much smaller than the thickness of the line).
EDIT: Indeed using a single bezier arc looks much worse than I would have expected even for reasonably normal cases. I tried also using two bezier arcs for each side and the result are better but still not perfect. The error is of course much smaller than the thickness of the line so unless lines are very thick it could be a reasonable option. In the following picture it's shown a thickened bezier (with per-point thickening), an approximation using a single bezier arc for each side and an approximation using two bezier arcs for each side.
EDIT 2: As requested I add the code I used to get the pictures; it's in python and requires only Qt. This code wasn't meant to be read by others so I used some tricks that probably I wouldn't use in real production code. The algorithm is also very inefficient but I didn't care about speed (this was meant to be a one-shot program to see if the idea works).
#
# This code has been written during an ego-pumping session on
# www.stackoverflow.com, while trying to reply to an interesting
# question. Do whatever you want with it but don't blame me if
# doesn't do what *you* think it should do or even if doesn't do
# what *I* say it should do.
#
# Comments of course are welcome...
#
# Andrea "6502" Griffini
#
# Requirements: Qt and PyQt
#
import sys
from PyQt4.Qt import *
QW = QWidget
bezlevels = 5
def avg(a, b):
"""Average of two (x, y) points"""
xa, ya = a
xb, yb = b
return ((xa + xb)*0.5, (ya + yb)*0.5)
def bez3split(p0, p1, p2,p3):
"""
Given the control points of a bezier cubic arc computes the
control points of first and second half
"""
p01 = avg(p0, p1)
p12 = avg(p1, p2)
p23 = avg(p2, p3)
p012 = avg(p01, p12)
p123 = avg(p12, p23)
p0123 = avg(p012, p123)
return [(p0, p01, p012, p0123),
(p0123, p123, p23, p3)]
def bez3(p0, p1, p2, p3, levels=bezlevels):
"""
Builds a bezier cubic arc approximation using a fixed
number of half subdivisions.
"""
if levels <= 0:
return [p0, p3]
else:
(a0, a1, a2, a3), (b0, b1, b2, b3) = bez3split(p0, p1, p2, p3)
return (bez3(a0, a1, a2, a3, levels-1) +
bez3(b0, b1, b2, b3, levels-1)[1:])
def thickPath(pts, d):
"""
Given a polyline and a distance computes an approximation
of the two one-sided offset curves and returns it as two
polylines with the same number of vertices as input.
NOTE: Quick and dirty approach, just uses a "normal" for every
vertex computed as the perpendicular to the segment joining
the previous and next vertex.
No checks for self-intersections (those happens when the
distance is too big for the local curvature), and no check
for degenerate input (e.g. multiple points).
"""
l1 = []
l2 = []
for i in xrange(len(pts)):
i0 = max(0, i - 1) # previous index
i1 = min(len(pts) - 1, i + 1) # next index
x, y = pts[i]
x0, y0 = pts[i0]
x1, y1 = pts[i1]
dx = x1 - x0
dy = y1 - y0
L = (dx**2 + dy**2) ** 0.5
nx = - d*dy / L
ny = d*dx / L
l1.append((x - nx, y - ny))
l2.append((x + nx, y + ny))
return l1, l2
def dist2(x0, y0, x1, y1):
"Squared distance between two points"
return (x1 - x0)**2 + (y1 - y0)**2
def dist(x0, y0, x1, y1):
"Distance between two points"
return ((x1 - x0)**2 + (y1 - y0)**2) ** 0.5
def ibez(pts, levels=bezlevels):
"""
Inverse-bezier computation.
Given a list of points computes the control points of a
cubic bezier arc that approximates them.
"""
#
# NOTE:
#
# This is a very specific routine that only works
# if the input has been obtained from the computation
# of a bezier arc with "levels" levels of subdivisions
# because computes the distance as the maximum of the
# distances of *corresponding points*.
# Note that for "big" changes in the input from the
# original bezier I dont't think is even true that the
# best parameters for a curve-curve match would also
# minimize the maximum distance between corresponding
# points. For a more general input a more general
# path-path error estimation is needed.
#
# The minimizing algorithm is a step descent on the two
# middle control points starting with a step of about
# 1/10 of the lenght of the input to about 1/1000.
# It's slow and ugly but required no dependencies and
# is just a bunch of lines of code, so I used that.
#
# Note that there is a closed form solution for finding
# the best bezier approximation given starting and
# ending points and a list of intermediate parameter
# values and points, and this formula also could be
# used to implement a much faster and accurate
# inverse-bezier in the general case.
# If you care about the problem of inverse-bezier then
# I'm pretty sure there are way smarter methods around.
#
# The minimization used here is very specific, slow
# and not so accurate. It's not production-quality code.
# You have been warned.
#
# Start with a straight line bezier arc (surely not
# the best choice but this is just a toy).
x0, y0 = pts[0]
x3, y3 = pts[-1]
x1, y1 = (x0*3 + x3) / 4.0, (y0*3 + y3) / 4.0
x2, y2 = (x0 + x3*3) / 4.0, (y0 + y3*3) / 4.0
L = sum(dist(*(pts[i] + pts[i-1])) for i in xrange(len(pts) - 1))
step = L / 10
limit = step / 100
# Function to minimize = max((a[i] - b[i])**2)
def err(x0, y0, x1, y1, x2, y2, x3, y3):
return max(dist2(*(x+p)) for x, p in zip(pts, bez3((x0, y0), (x1, y1),
(x2, y2), (x3, y3),
levels)))
while step > limit:
best = None
for dx1 in (-step, 0, step):
for dy1 in (-step, 0, step):
for dx2 in (-step, 0, step):
for dy2 in (-step, 0, step):
e = err(x0, y0,
x1+dx1, y1+dy1,
x2+dx2, y2+dy2,
x3, y3)
if best is None or e < best[0] * 0.9999:
best = e, dx1, dy1, dx2, dy2
e, dx1, dy1, dx2, dy2 = best
if (dx1, dy1, dx2, dy2) == (0, 0, 0, 0):
# We got to a minimum for this step => refine
step *= 0.5
else:
# We're still moving
x1 += dx1
y1 += dy1
x2 += dx2
y2 += dy2
return [(x0, y0), (x1, y1), (x2, y2), (x3, y3)]
def poly(pts):
"Converts a list of (x, y) points to a QPolygonF)"
return QPolygonF(map(lambda p: QPointF(*p), pts))
class Viewer(QW):
def __init__(self, parent):
QW.__init__(self, parent)
self.pts = [(100, 100), (200, 100), (200, 200), (100, 200)]
self.tracking = None # Mouse dragging callback
self.ibez = 0 # Thickening algorithm selector
def sizeHint(self):
return QSize(900, 700)
def wheelEvent(self, e):
# Moving the wheel changes between
# - original polygonal thickening
# - single-arc thickening
# - double-arc thickening
self.ibez = (self.ibez + 1) % 3
self.update()
def paintEvent(self, e):
dc = QPainter(self)
dc.setRenderHints(QPainter.Antialiasing)
# First build the curve and the polygonal thickening
pts = bez3(*self.pts)
l1, l2 = thickPath(pts, 15)
# Apply inverse bezier computation if requested
if self.ibez == 1:
# Single arc
l1 = bez3(*ibez(l1))
l2 = bez3(*ibez(l2))
elif self.ibez == 2:
# Double arc
l1 = (bez3(*ibez(l1[:len(l1)/2+1], bezlevels-1)) +
bez3(*ibez(l1[len(l1)/2:], bezlevels-1))[1:])
l2 = (bez3(*ibez(l2[:len(l2)/2+1], bezlevels-1)) +
bez3(*ibez(l2[len(l2)/2:], bezlevels-1))[1:])
# Draw results
dc.setBrush(QBrush(QColor(0, 255, 0)))
dc.drawPolygon(poly(l1 + l2[::-1]))
dc.drawPolyline(poly(pts))
dc.drawPolyline(poly(self.pts))
# Draw control points
dc.setBrush(QBrush(QColor(255, 0, 0)))
dc.setPen(QPen(Qt.NoPen))
for x, y in self.pts:
dc.drawEllipse(QRectF(x-3, y-3, 6, 6))
# Display the algorithm that has been used
dc.setPen(QPen(QColor(0, 0, 0)))
dc.drawText(20, 20,
["Polygonal", "Single-arc", "Double-arc"][self.ibez])
def mousePressEvent(self, e):
# Find closest control point
i = min(range(len(self.pts)),
key=lambda i: (e.x() - self.pts[i][0])**2 +
(e.y() - self.pts[i][1])**2)
# Setup a callback for mouse dragging
self.tracking = lambda p: self.pts.__setitem__(i, p)
def mouseMoveEvent(self, e):
if self.tracking:
self.tracking((e.x(), e.y()))
self.update()
def mouseReleaseEvent(self, e):
self.tracking = None
# Qt boilerplate
class MyDialog(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.ws = Viewer(self)
L = QVBoxLayout(self)
L.addWidget(self.ws)
self.setModal(True)
self.show()
app = QApplication([])
aa = MyDialog(None)
aa.exec_()
aa = None

Resources