I have created a Python file to generate a Mandelbrot set image. The original maths code was not mine, so I do not understand it - I only heavily modified it to make it about 250x faster (Threads rule!).
Anyway, I was wondering how I could modify the maths part of the code to make it render one specific bit. Here is the maths part:
for y in xrange(size[1]):
coords = (uleft[0] + (x/size[0]) * (xwidth),uleft[1] - (y/size[1]) * (ywidth))
z = complex(coords[0],coords[1])
o = complex(0,0)
dotcolor = 0 # default, convergent
for trials in xrange(n):
if abs(o) <= 2.0:
o = o**2 + z
else:
dotcolor = trials
break # diverged
im.putpixel((x,y),dotcolor)
And the size definitions:
size1 = 500
size2 = 500
n=64
box=((-2,1.25),(0.5,-1.25))
plus = size[1]+size[0]
uleft = box[0]
lright = box[1]
xwidth = lright[0] - uleft[0]
ywidth = uleft[1] - lright[1]
what do I need to modify to make it render a certain section of the set?
The line:
box=((-2,1.25),(0.5,-1.25))
is the bit that defines the area of coordinate space that is being rendered, so you just need to change this line. First coordinate pair is the top-left of the area, the second is the bottom right.
To get a new coordinate from the image should be quite straightforward. You've got two coordinate systems, your "image" system 100x100 pixels in size, origin at (0,0). And your "complex" plane coordinate system defined by "box". For X:
X_complex=X_complex_origin+(X_image/X_image_width)*X_complex_width
The key in understanding how to do this is to understand what the coords = line is doing:
coords = (uleft[0] + (x/size[0]) * (xwidth),uleft[1] - (y/size[1]) * (ywidth))
Effectively, the x and y values you are looping through which correspond to the coordinates of the on-screen pixel are being translated to the corresponding point on the complex plane being looked at. This means that (0,0) screen coordinate will translate to the upper left region being looked at (-2,1.25), and (1,0) will be the same, but moved 1/500 of the distance (assuming a 500 pixel width window) between the -2 and 0.5 x-coordinate.
That's exactly what that line is doing - I'll expand just the X-coordinate bit with more illustrative variable names to indicate this:
mandel_x = mandel_start_x + (screen_x / screen_width) * mandel_width
(The mandel_ variables refer to the coordinates on the complex plane, the screen_ variables refer to the on-screen coordinates of the pixel being plotted.)
If you want then to take a region of the screen to zoom into, you want to do exactly the same: take the screen coordinates of the upper-left and lower-right region, translate them to the complex-plane coordinates, and make those the new uleft and lright variables. ie to zoom in on the box delimited by on-screen coordinates (x1,y1)..(x2,y2), use:
new_uleft = (uleft[0] + (x1/size[0]) * (xwidth), uleft[1] - (y1/size[1]) * (ywidth))
new_lright = (uleft[0] + (x2/size[0]) * (xwidth), uleft[1] - (y2/size[1]) * (ywidth))
(Obviously you'll need to recalculate the size, xwidth, ywidth and other dependent variables based on the new coordinates)
In case you're curious, the maths behind the mandelbrot set isn't that complicated (just complex).
All it is doing is taking a particular coordinate, treating it as a complex number, and then repeatedly squaring it and adding the original number to it.
For some numbers, doing this will cause the result diverge, constantly growing towards infinity as you repeat the process. For others, it will always stay below a certain level (eg. obviously (0.0, 0.0) never gets any bigger under this process. The mandelbrot set (the black region) is those coordinates which don't diverge. Its been shown that if any number gets above the square root of 5, it will diverge - your code is just using 2.0 as its approximation to sqrt(5) (~2.236), but this won't make much noticeable difference.
Usually the regions that diverge get plotted with the number of iterations of the process that it takes for them to exceed this value (the trials variable in your code) which is what produces the coloured regions.
Related
I am trying to pack hard-spheres in a unit cubical box, such that these spheres cannot overlap on each other. This is being done in Python.
I am given some packing fraction f, and the number of spheres in the system is N.
So, I say that the diameter of each sphere will be
d = (p*6/(math.pi*N)**)1/3).
My box has periodic boundary conditions - which means that there is a recurring image of my box in all direction. If there is a particle who is at the edge of the box and has a portion of it going beyond the wall, it will stick out at the other side.
My attempt:
Create a numpy N-by-3 array box which holds the position vector of each particle [x,y,z]
The first particle is fine as it is.
The next particle in the array is checked with all the previous particles. If the distance between them is more than d, move on to the next particle. If they overlap, randomly change the position vector of the particle in question. If the new position does not overlap with the previous atoms, accept it.
Repeat steps 2-3 for the next particle.
I am trying to populate my box with these hard spheres, in the following manner:
for i in range(1,N):
mybool=True
print("particles in box: " + str(i))
while (mybool): #the deal with this while loop is that if we place a bad particle, we need to change its position, and restart the process of checking
for j in range(0,i):
displacement=box[j,:]-box[i,:]
for k in range(3):
if abs(displacement[k])>L/2:
displacement[k] -= L*np.sign(displacement[k])
distance = np.linalg.norm(displacement,2) #check distance between ith particle and the trailing j particles
if distance<diameter:
box[i,:] = np.random.uniform(0,1,(1,3)) #change the position of the ith particle randomly, restart the process
break
if j==i-1 and distance>diameter:
mybool = False
break
The problem with this code is that if p=0.45, it is taking a really, really long time to converge. Is there a better method to solve this problem, more efficiently?
I think what you are looking for is either the hexagonal closed-packed (HCP or sometime called face-centered cubic, FCC) lattice or the cubic closed-packed one (CCP). See e.g. Wikipedia on Close-packing of equal spheres.
Since your space has periodic conditions, I believe it doesn't matter which one you chose (hcp or ccp), and they both achieve the same density of ~74.04%, which was proved by Gauss to be the highest density by lattice packing.
Update:
For the follow-up question on how to generate efficiently one such lattice, let's take as an example the HCP lattice. First, let's create a bunch of (i, j, k) indices [(0,0,0), (1,0,0), (2,0,0), ..., (0,1,0), ...]. Then, get xyz coordinates from those indices and return a DataFrame with them:
def hcp(n):
dim = 3
k, j, i = [v.flatten()
for v in np.meshgrid(*([range(n)] * dim), indexing='ij')]
df = pd.DataFrame({
'x': 2 * i + (j + k) % 2,
'y': np.sqrt(3) * (j + 1/3 * (k % 2)),
'z': 2 * np.sqrt(6) / 3 * k,
})
return df
We can plot the result as scatter3d using plotly for interactive exploration:
import plotly.graph_objects as go
df = hcp(12)
fig = go.Figure(data=go.Scatter3d(
x=df.x, y=df.y, z=df.z, mode='markers',
marker=dict(size=df.x*0 + 30, symbol="circle", color=-df.z, opacity=1),
))
fig.show()
Note: plotly's scatter3d is not a very good rendering of spheres: the marker sizes are constant (so when you zoom in and out, the "spheres" will appear to change relative size), and there is no shading, limited z-ordering faithfulness, etc., but it's convenient to interact with the plot.
Resize and clip to the unit box:
Here, a strict clipping (each sphere needs to be completely inside the unit box). Your "periodic boundary condition" is something you will need to address separately (see further below for ideas).
def hcp_unitbox(r):
n = int(np.ceil(1 / (np.sqrt(3) * r)))
df = hcp(n) * r
df += r
df = df[(df <= 1 - r).all(axis=1)]
return df
With this, you find that a radius of 0.06 gives you 608 fully enclosed spheres:
hcp_unitbox(.06).shape # (608, 3)
Where you would go next:
You may dig deeper into the effect of your so-called "periodic boundary conditions", and perhaps play with some rotations (and small translations).
To do so, you may try to generate an HCP-lattice that is large enough that any rotation will still fully enclose your unit cube. For example:
r = 0.2 # example
n = int(np.ceil(2 / r))
df = hcp(n) * r - 1
Then rotate it (by any amount) and translate it (by up to 1 radius in any direction) as you wish for your research, and clip. The "periodic boundary conditions", as you call them, present a bit of extra challenge, as the clipping becomes trickier. First, clip any sphere whose center is outside your box. Then select spheres close enough to the boundaries, or even partition the regions of interest into overlapping regions along the walls of your cube, then check for collisions among the spheres (as per your periodic boundary conditions) that fall in each such region.
I am performing motion tracking of an object, and I am trying to identify the front and back of the object. The object is asymmetrical, which means that the centroid of the contour is closer to the front than the back. Using this information, I am approaching this as follows:
Draw contours of object
Find centroid
centroidx, centroidy = int(moments['m10']/moments['m00']), int(moments['m10']/moments['m00'])
Draw bounding ellipse
cv2.fitEllipse(contour)
Calculate major axis length as follows (and as shown in the figure)
MAx, MAy = int(0.5 * ellipseMajorAxisx*math.sin(ellipseAngle)), int(0.5 * ellipseMajorAxisy*math.cos(ellipseAngle))
Calculate beginning and ending x, y coordinates of the major axis
MAxtop, MAytop = int(ellipseCentrex + MAx), int(ellipseCentrey + MAy)
MAxbot, MAybot = int(ellipseCentrex - MAx), int(ellipseCentrey - MAy)
Identify which of the points is closer to the centroid of the contour
distancetop = math.sqrt((centroidx - MAxtop)**2 + (centroidy - MAytop)**2)
distancebot = math.sqrt((centroidx - MAxbot)**2 + (centroidy - MAybot)**2)
min(distancetop, distancebot)
The problem I am encountering is, while I get the "front" end of the ellipse correct most of the time, occasionally the point is a little bit away. As far as I have observed, this seems to be happening such that the x value is correct, but y value is different (in effect, I think this represents the major axis of an ellipse that is perpendicular to mine). I am not sure if this is an issue with opencv's calculation of angles or (more than likely) my calculations are incorrect. I do realize this is a complicated example, hope my figures help!
EDIT: When I get the wrong point, it is not from a perpendicular ellipse, but of a mirror image of my ellipse. And it happens with the x values too, not just y.
After following ssm's suggestion below, I am getting the desired point most of the time. The point still goes wrong occasionally, but "snaps back" into place soon after. For example, this is a few frames when this happens:
By the way, the above images are after "correcting" for angle by using this code:
if angle > 90:
angle = 180 - angle
If I do not do the correction, I get the wrong point at other times, as shown below for the same frames.
So it looks like I get it right for some angles with angle correction and the other angles without correction. How do I get all the right points in both conditions?
(White dot inside the ellipse is the centroid of the contour, whereas the dot on or outside the ellipse is the point I am getting)
I think your only problem is MAytop. You can consider doing the following:
if ycen<yc:
# switch MAytop and MAybot
temp = MAytop
MAytop = MAybot
MAybot = temp
You may have to do a similar check on the x - scale
This might seem a bit strange, but I really feel like there should be a relatively straightforward solution to it. Basically I've got an image in the form of a 3D numpy array (x, y, color). I was following along with this tutorial for a slightly different product area, and found that these methods did not extend well.
As a result, I'm making a modified edge detection algorithm for my use case. As of now this is just some basic signal processing on top of a 1d array. This works great if I only want to sample in the x and y directions, as I can just use the existing rows and columns of the array.
However, to determine orientation of these edges, I would like to be able to sample any arbitrary vector across the image below is an image to help illustrate:
I tried hacking together something that would just append pixels as it crossed them, but it was inefficient, inelegant, and non-ideal in a number of ways. I feel like there must be some relatively elegant way of doing this.
Any ideas? The size of the sample across the vector doesn't really matter to me if that makes things any easier.
I would make an equation for the line you want to cut along, then make a mask around it and keep all pixels that come within some width of it. For example, say you want a cut along i = 2*j + 34, where i and j are measured in pixels:
h, w = im.shape[:2]
width = 2 # width of slice in pixels, too narrow and it will have gaps
i, j = np.ogrid[:h, :w]
mask = np.abs(2*j + 34 - i) < width
im[mask]
Note that im[mask] will be a 2d array, since it should still have the colors. It will be ordered so that the uppermost pixels are first, and the bottom pixels are last, opposite of that shown in your arrow, unless of course you have origin=lower in your plotting :) And if several pixels are selected in each row (if width > 1), then they'll go left to right, so the shape for a slice like your drawing would be a tiny sequence of z's, and for the other direction, backwards z's (s's?).
Keep in mind that for an array there doesn't exist a diagonal slice without some weird zigzag (or alternatively, interpolation) no matter how elegant your implementation is. You could rotate the image (by some algorithm) and take a horizontal slice.
Using the equation
x2 = x1 + length * cos(θ)
y2 = y1 + length * sin(θ)
where
θ = angle * 3.14 / 180.0
You can iterate through the pixel using angle and length like
int angle =45; //angle of iteration
int length = 0; //Alternately you can skip the pixel by giving value other than 0
Point P1(starX,startY); //Your starting point.
Point P2;//??
while(1){
length++;
P2.x = (int)round(P1.x + length * cos(angle * CV_PI / 180.0));
P2.y = (int)round(P1.y + length * sin(angle * CV_PI / 180.0));
if(P2_exceed_boundary()) break;
do_Whatever_with_P2();
}
I am trying to estimate the value of pi using a monte carlo simulation. I need to use two unit circles that are a user input distance from the origin. I understand how this problem works with a single circle, I just don't understand how I am meant to use two circles. Here is what I have got so far (this is the modified code I used for a previous problem the used one circle with radius 2.
import random
import math
import sys
def main():
numDarts=int(sys.argv[1])
distance=float(sys.argv[2])
print(montePi(numDarts,distance))
def montePi(numDarts,distance):
if distance>=1:
return(0)
inCircle=0
for I in range(numDarts):
x=(2*(random.random()))-2
y=random.random()
d=math.sqrt(x**2+y**2)
if d<=2 and d>=-2:
inCircle=inCircle+1
pi=inCircle/numDarts*4
return pi
main()
I need to change this code to work with 2 unit circles, but I do not understand how to use trigonometry to do this, or am I overthinking the problem? Either way help will be appreciated as I continue trying to figure this out.
What I do know is that I need to change the X coordinate, as well as the equation that determines "d" (d=math.sqrt(x*2+y*2)), im just not sure how.
These are my instructions-
Write a program called mcintersection.py that uses the Monte Carlo method to
estimate the area of this shape (and prints the result). Your program should take
two command-line parameters: distance and numDarts. The distance parameter
specifies how far away the circles are from the origin on the x-axis. So if distance
is 0, then both circles are centered on the origin, and completely overlap. If
distance is 0.5 then one circle is centered at (-0.5, 0) and the other at (0.5, 0). If
distance is 1 or greater, then the circles do not overlap at all! In that last case, your
program can simply output 0. The numDarts parameter should specify the number
of random points to pick in the Monte Carlo process.
In this case, the rectangle should be 2 units tall (with the top at y = 1 and the
bottom at y = -1). You could also safely make the rectangle 2 units wide, but this
will generally be much bigger than necessary. Instead, you should figure out
exactly how wide the shape is, based on the distance parameter. That way you can
use as skinny a rectangle as possible.
If I understand the problem correctly, you have two unit circles centered at (distance, 0) and (-distance, 0) (that is, one is slightly to the right of the origin and one is slightly to the left). You're trying to determine if a given point, (x, y) is within both circles.
The simplest approach might be to simply compute the distance between the point and the center of each of the circles. You've already done this in your previous code, just repeat the computation twice, once with the offset distance inverted, then use and to see if your point is in both circles.
But a more elegant solution would be to notice how your two circles intersect each other exactly on the y-axis. To the right of the axis, the left circle is completely contained within the right one. To the left of the y-axis, the right circle is entirely within the left circle. And since the shape is symmetrical, the two halves are of exactly equal size.
This means you can limit your darts to only hitting on one side of the axis, and then get away with just a single distance test:
def circle_intersection_area(num_darts, distance):
if distance >= 1:
return 0
in_circle = 0
width = 1-distance # this is enough to cover half of the target
for i in range(num_darts):
x = random.random()*width # random value from 0 to 1-distance
y = random.random()*2 - 1 # random value from -1 to 1
d = math.sqrt((x+distance)**2 + y**2) # distance from (-distance, 0)
if d <= 1:
in_circle += 1
sample_area = width * 2
target_area = sample_area * (in_circle / num_darts)
return target_area * 2 # double, since we were only testing half the target
I am trying to create a map editor. I intend the map to be an hexagonal grid where each hexagon is a tile of the map. A tile will be a graphical representation of that area (sea, meadow, desert, mountain, etc). The map is intended to be of any size. Let's freeze the requirements here for now :)
I want to use PyQt4 (take it as a design requirement). As I am just starting with Qt/PyQt, I am facing the problem of vastness: so big this Qt thing that I cannot grasp it all. And here I am, asking for your kind and most welcome experience.
After a bit of googling, I've decided to use the QGraphicalView/Scene approach. In fact, I was thinking about creating my own hexgrid class inheriting from QGraphicalView and creating my RegularPolygon class inheriting from QGraphicalPolygonItem.
Now they come the doubts and problems.
My main doubt is "Is my approach a correct one?" Think about the needs I have explained at the beginning of the post: hexagonal map, where each hexagon will be a tile of a given type (sea, desert, meadows, mountains, etc). I am concerned about performance once the editor works (scrolling will feel nice? and this kind of things).
And so far, the problem is about precision. I am drawing the hexgrid by creating and drawing all its hexagons (this even sounds bad to me... thinking about performance). I used some formulas to calculate the vertices of each hexagon and creating the polygon from there. I expect the sides of two consecutive hexagons to coincide exactly at the same location, but the rounding seems to be playing a bit with my desires, as sometimes the hexagon sides perfectly matches in the same location (good) and sometimes they do not match by what seems to be 1 pixel difference (bad). This gives a poor visual impression of the grid. Maybe I have not explained myself quite well... it's better if I give you the code and you run it by yourselves
So summarizing:
Do you think my approach will give future performance issues?
Why are not the hexagons placed exactly so that they share sides? How to avoid this problem?
The code:
#!/usr/bin/python
"""
Editor of the map.
"""
__meta__ = \
{
(0,0,1): (
[ "Creation" ],
[ ("Victor Garcia","vichor#xxxxxxx.xxx") ]
)
}
import sys, math
from PyQt4 import QtCore, QtGui
# ==============================================================================
class HexGrid(QtGui.QGraphicsView):
"""
Graphics view for an hex grid.
"""
# --------------------------------------------------------------------------
def __init__(self, rect=None, parent=None):
"""
Initializes an hex grid. This object will be a GraphicsView and it will
also handle its corresponding GraphicsScene.
rect -- rectangle for the graphics scene.
parent -- parent widget
"""
super(HexGrid,self).__init__(parent)
self.scene = QtGui.QGraphicsScene(self)
if rect != None:
if isinstance(rect, QtCore.QRectF): self.scene.setSceneRect(rect)
else: raise StandardError ('Parameter rect should be QtCore.QRectF')
self.setScene(self.scene)
# ==============================================================================
class QRegularPolygon(QtGui.QGraphicsPolygonItem):
"""
Regular polygon of N sides
"""
def __init__(self, sides, radius, center, angle = None, parent=None):
"""
Initializes an hexagon of the given radius.
sides -- sides of the regular polygon
radius -- radius of the external circle
center -- QPointF containing the center
angle -- offset angle in radians for the vertices
"""
super(QRegularPolygon,self).__init__(parent)
if sides < 3:
raise StandardError ('A regular polygon at least has 3 sides.')
self._sides = sides
self._radius = radius
if angle != None: self._angle = angle
else: self._angle = 0.0
self._center = center
points = list()
for s in range(self._sides):
angle = self._angle + (2*math.pi * s/self._sides)
x = center.x() + (radius * math.cos(angle))
y = center.y() + (radius * math.sin(angle))
points.append(QtCore.QPointF(x,y))
self.setPolygon( QtGui.QPolygonF(points) )
# ==============================================================================
def main():
"""
That's it: the main function
"""
app = QtGui.QApplication(sys.argv)
grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0))
radius = 50
sides = 6
apothem = radius * math.cos(math.pi/sides)
side = 2 * apothem * math.tan(math.pi/sides)
xinit = 50
yinit = 50
angle = math.pi/2
polygons = list()
for x in range(xinit,xinit+20):
timesx = x - xinit
xcenter = x + (2*apothem)*timesx
for y in range(yinit, yinit+20):
timesy = y - yinit
ycenter = y + ((2*radius)+side)*timesy
center1 = QtCore.QPointF(xcenter,ycenter)
center2 = QtCore.QPointF(xcenter+apothem,ycenter+radius+(side/2))
h1 = QRegularPolygon(sides, radius, center1, angle)
h2 = QRegularPolygon(sides, radius, center2, angle)
# adding polygons to a list to avoid losing them when outside the
# scope (loop?). Anyway, just in case
polygons.append(h1)
polygons.append(h2)
grid.scene.addItem(h1)
grid.scene.addItem(h2)
grid.show()
app.exec_()
# ==============================================================================
if __name__ == '__main__':
main()
and last but not least, sorry for the long post :)
Thanks
Victor
Personally, I'd define each hexagonal tile as a separate SVG image, and use QImage and QSvgRenderer classes to render them to QPixmaps (with an alpha channel) whenever the zoom level changes. I'd create a QGraphicsItem subclass for displaying each tile.
The trick is to pick the zoom level so that the width of the (upright) hexagon is a multiple of two, and the height a multiple of four, with width/height approximately sqrt(3/4). The hexagons are slightly squished in either direction, but for all hexagons at least eight pixels in diameter, the effect is inperceptible.
If the width of the hexagon is 2*w, and height 4*h, here's how to map the (upright) hexagons to Cartesian coordinates:
If each side of the hexagon is a, then h=a/2 and w=a*sqrt(3)/2, therefore w/h=sqrt(3).
For optimum display quality, pick integer w and h, so that their ratio is approximately sqrt(3) ≃ 1.732. This means your hexagons will be very slightly squished, but that's okay; it is not perceptible.
Because the coordinates are now always integers, you can safely (without display artefacts) use pre-rendered hexagon tiles, as long as they have an alpha channel, and perhaps a border to allow smoother alpha transitions. Each rectangular tile is then 2*w+2*b pixels wide and 4*h+2*b pixels tall, where b is the number of extra border (overlapping) pixels.
The extra border is needed to avoid perceptible seams (background color bleeding through) where pixels are only partially opaque in all overlapping tiles. The border allows you to better blend the tile into the neighboring tile; something the SVG renderer will do automatically if you include a small border region in your SVG tiles.
If you use typical screen coordinates where x grows right and y down, then the coordinates for hexagon X,Y relative to the 0,0 one are trivial:
y = 3*h*Y
if Y is even, then:
x = 2*w*X
else:
x = 2*w*X + w
Obviously, odd rows of hexagons are positioned half a hexagon to the right.
Subclassing QGraphicsItem and using a bounding polygon (for mouse and interaction tests) means Qt will do all the heavy work for you, when you wish to know which hexagonal tile the mouse is hovering on top of.
However, you can do the inverse mapping -- from screen coordinates back to hexagons -- yourself.
First, you calculate which rectangular grid cell (green grid lines in the image above) the coordinate pair is in:
u = int(x / w)
v = int(y / h)
Let's assume all coordinates are nonnegative. Otherwise, % must be read as "nonnegative remainder, when divided by". (That is, 0 <= a % b < b for all a, even negative a; b is always a positive integer here.)
If the origin is as shown in the above image, then two rows out of every three are trivial, except that every odd row of hexagons is shifted one grid cell right:
if v % 3 >= 1:
if v % 6 >= 4:
X = int((u - 1) / 2)
Y = int(v / 3)
else:
X = int(u / 2)
Y = int(v / 3)
Every third row contains rectangular grid cells with a diagonal boundary, but worry not: if the boundary is \ (wrt. above image), you only need to check if
(x % w) * h >= (y % h) * w
to find out if you are in the upper right triangular part. If the boundary is / wrt. above image, you only need to check if
(x % w) * h + (y % h) * w >= (w * h - (w + h) / 2)
to find out if you are in the lower right triangular part.
In each four-column and six-row section of rectangular grid cells, there are eight cases that need to be handled, using one of the above test clauses. (I'm too lazy to work the exact if clauses for you here; like I said, I'd let Qt do that for me.) This rectangular region repeats exactly for the entire hexagonal map; thus, a full coordinate conversion may need up to 9 if clauses (depending on how you write it), so it's a bit annoying to write.
If you wish to determine e.g. the mouse cursor location relative to the hexagon it is hovering over, first use the above to determine which hexagon the mouse hovers over, then substract the coordinates of that hexagon from the mouse coordinates to get the coordinates relative to the current hexagon.
Try with this main() function. I used the radius of the inscribed circle (ri) instead of the circumscribed circle that you used (radius). It looks a bit better now, but still not perfect. I think the way the oblique sides are drawn at the top and bottom of the hexagon are different.
def main():
"""
That's it: the main function
"""
app = QtGui.QApplication(sys.argv)
grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0))
radius = 50 # circumscribed circle radius
ri = int(radius / 2 * math.sqrt(3)) # inscribed circle radius
sides = 6
apothem = int(ri * math.cos(math.pi/sides))
side = int(2 * apothem * math.tan(math.pi/sides))
xinit = 50
yinit = 50
angle = math.pi/2
polygons = list()
for x in range(xinit,xinit+20):
timesx = x - xinit
xcenter = x + (2*apothem-1)*timesx
for y in range(yinit, yinit+20):
timesy = y - yinit
ycenter = y + ((2*ri)+side)*timesy
center1 = QtCore.QPointF(xcenter,ycenter)
center2 = QtCore.QPointF(xcenter+apothem,ycenter+ri+(side/2))
h1 = QRegularPolygon(sides, ri, center1, angle)
h2 = QRegularPolygon(sides, ri, center2, angle)
# adding polygons to a list to avoid losing them when outside the
# scope (loop?). Anyway, just in case
polygons.append(h1)
polygons.append(h2)
grid.scene.addItem(h1)
grid.scene.addItem(h2)
grid.show()
app.exec_()
There are multiple problems here. They aren't specifically related to Qt or to Python, but to general computer science.
You have floating point geometrical shapes that you want to display on a raster device, so somehow there has to be a floating point to integer conversion. It's not in your code, so it will happen at a lower level: in the graphics library, the display driver or whatever. Since you're not happy with the result, you have to handle this conversion yourself.
There's no right or wrong way to do this. For example, take your case of a hex tile that has a “radius” of 50. The hexagon is oriented so that the W vertex is at (-50,0) and the E vertex is at (50,0). Now the NE vertex of this hexagon is at approximately (25,0,43.3). The hexagon that's adjacent to this one in the N direction has its center at about y=86.6 and its top edge at 129.9. How would you like to pixellate this? If you round 43.3 down to 43, now you no longer have a mathematically exact regular hexagon. If you round 129.9 up to 130, your first hexagon is 86 pixels in total height but the one on top of it is 87. This is an issue that you must resolve based on the project's requirements.
And this is just one case (radius=50). If you allow the radius to be variable, can you come up with an algorithm to handle all cases? I couldn't. I think you need to use a fixed screen dimension for your hexagons, or at least reduce the possibilities to a small number.
Nowhere in your code do you determine the size of the display window, so I don't understand how you intend to handle scaling issues, or determine how many hexes are needed to show the full map.
As to your first question, I am certain that the performance will be poor. The constructor for QRegularPolygon is inside the loop that creates the hexes, so it gets called many times (800 in your example). It does two trig calculations for each vertex, so you perform 9600 trig calculations as you build your list of hexes. You don't need ANY of them. The calculations are the sine and cosine of 0 degrees, 60 degrees, 120 degrees and so on. Those are so easy you don't even need sin and cos.
The use of the trig functions exacerbates the floating point/integer problem, too. Look at this:
>> int(50.0*math.sin(math.pi/6))
24
We know it should be 25, but the computer figures it as int(24.999999999996) – I may have left out a few 9's.
If you calculate the vertex positions of just one hexagon, you can get all the other ones by a simple translation. See the useful Qt functions QPolygon->translate or QPolygon->translated.
It seems that you don't need a constructor that can handle any type of polygon when your design concept absolutely needs hexagons. Did you just copy it from somewhere? I think it's mostly clutter, which always opens the door to errors.
Do you really need polygons here? Later on, I suppose, the game will use raster images, so the polygons are just for display purposes.
You could just take a point cloud representing all corners of the polygon and draw lines beneath them. With this, you avoid problems of rounding / floating point arithmetics etc.