Failed to display all the spheres in perspetive projection of 3D animation - python

I have generated an optic flow animation, with spheres (circles) that move towards the viewer in a 3D coordinates space. For some reason, although I define a number of 8 spheres, it never displays all the spheres every time I run the code; sometimes it displays 1, sometimes 4 (like in the gif). It is eventually a random number of spheres from 1 to 8.
My code is available on Github

At perspective projection, the viewing volume is a frustum. So probably the spheres are clipped (not in the frustum) at the sides of the frustum, especially when they are close to the near plane.
Note, most of the stars "leave" the window at its borders, when they come closer to the camera (except the ones, who leave the frustum through the near plane).
Set the initial z-coordinate of the spheres to it's maximum (far plane), for debug reasons:
for sphere in spheres:
sphere.position.xy = np.random.uniform(-25, 25, size=2)
#sphere.position.z = np.random.uniform(0.0, -50.0)
sphere.position.z = 50
If you don't "see" all stars at all, then the range for the x and y coordinate ([-25, 25]) is to large.
To compensate the in initial clipping you can scale the x and y component by the distance:
for sphere in spheres:
sphere.position.xy = np.random.uniform(-25, 25, size=2)
z = np.random.uniform(0.0, -50.0)
sphere.position.z = z
sphere.position.xy[0] *= z/-50
sphere.position.xy[1] *= z/-50

Related

What units does Plotly camera center (layout.scene.camera.center) use?

In a 3D Plotly plot the camera center defaults to (0,0,0), where, as far as I understand, (0,0,0) refers to the centre of the 3D volume occupied by the plot, not the coordinate (0,0,0).
These values can be changed via layout.scene.camera.center as documented here and here. However, I can't work out what units are being used, nor can I find this information in the documentation.
E.g. if I change the camera center to (1,1,1), where is this in relation to my plot? From a bit of experimenting I have discovered that:
(1,1,1) puts the camera center outside the volume occupied by my plot, but I can't figure out how far outside,
(0.5, 0.5, 0.5) put the camera center near, but not exactly on, one of the edges of the volume occupied by the plot; sometimes it is near a corner of the volume, sometimes it is along an edge.
Note: I'm not 100% sure that my answer relates to plotly-python, but it works that way in plotly-js so I suppose it should be the same.
By default camera's center is set to (0, 0, 0), that is the visual center of your plot. So, assuming following edge values on axes:
x: [10, 110],
y: [0, 50],
z: [1, 11],
Center point will have coords of (60, 25, 6) (e.g. for x: (10 + 110) / 2 == 60).
To calculate camera coords corresponding to some point within your plot's axes, you can use the following formula (given example is for x axis, but is valid for any):
multiplier = 0.5 * aspectratio.x
x = ((point.x - center.x) / halfLengthOfAxisX) * multiplier
So, in our example, if we wanted to center the camera on point (1, 2, 3), given aspect ratio 1, we would have:
multiplier = 0.5
halfLengthOfAxisX = 50 // Math.abs(center.x - Math.min(x))
x = ((1 - 60) / 50) * 0.5 // -0.59
You mentioned that (0.5, 0.5, 0.5) puts the camera near, but not exactly on one of the edges. That's probably caused by not taking aspectratio into the consideration. From what I know there is no way to retrieve it if it's calculated by Plotly (at least using Plotly.js; it could work differently in Python), so you may need to set it manually.

Projected Area calculation of a cube

I am working on measuring the projected area of a cube facing the sun for my spacecraft coursework. The cube is of 1x1x1 dimensions, and constantly rotates due to its orbit. Using a program called "STK", data for the angle shift according to a reference was obtained. So now I have the shift of orientation of the cube every 30 minutes but now I need to calculate how much of the projected area will be exposed to the sun (I can assume the Sunlight is coming from a single direction).
I need to be able to translate the coordinate shift in orientation of the cube to how much of a projected area will be facing the sun at each interval of time. Let me give you an example:
At the initial time, the cube is facing you (you are the sun...because you are my sunshine ;) ) and no shift has occurred, hence the projected area will be 1 m^2.
After 30 mins, there has been a shift only on the x axis of 45 degrees. Now the projected area is 1.4142 m^2 (since cos 45 * 1 = 0.7071 and now you have 2 faces facing you).
After 60 mins, only a shift in the y axis occurs (45 degrees). Now you have 3 partial faces of the cube facing you and possess a projected area of 1.707 m^2.
This isn't to hard to do with little shifts, but I need to do this for multiple (more than a 100 shifts). I am thinking of writing a python program that rotates a 3D object and measures the projected area at each interval. Any recommendations on libraries that allow 3D body definition and rotation? libraries that can measure areas of projected surfaces?
Establish a unit vector perpendicular to each face of the cube. Depending on the output of your rotation program, you may be using angular rotations from the base axes or you can take vector cross product of 2 edges of face (be careful w/ right hand rule to ensure result faces outward)
take the dot product of each of the resultant 6 vectors individually with a vector "pointing to the sun"
drop any negative results (facing away from sun)
sum the remainder
Unit vectors will suffice because the surface area of each face is 1 sq unit.

Theory behind Wolfenstein-style 3D rendering

I'm currently working on a project about 3D rendering, and I'm trying to make simplistic program that can display a simple 3D room (static shading, no player movement, only rotation) with pygame
So far I've worked through the theory:
Start with a list of coordinates for the X and Z of each "Node"
Nodes are kept in an order which forms a closed loop, so that a pair of nodes will form either side of a wall
The height of the wall is determined when it is rendered, being relative to distance from the camera
Walls are rendered using painter's algorithm, so closer objects are drawn on top of further ones
For shading "fake contrast", which brightens/darkens walls based on the gradient between it's two nodes
While it seems simple enough, the process behind translating the 3D coordinates into 2D points on the screen is proving the difficult for me to understand.
Googling this topic has so far only yeilded these equations:
screenX = (worldX/worldZ)
screenY = (worldY/worldZ)
Which seem flawed to me, as you would get a divide by zero error if any Z coordinate is 0.
So if anyone could help explain this, I'd be really greatful.
Well the
screenX = (worldX/worldZ)
screenY = (worldY/worldZ)
is not the whole stuff that is just the perspective division by z and it is not meant for DOOM or Wolfenstein techniques.
Well in Doom there is only single angle of viewing (you can turn left/right but cannot look up/down only duck or jump which is not the same). So we need to know our player position and direction px,py,pz,pangle. The z is needed only if you want to implement also z axis movement/looking...
If you are looking in a straight line (Red) all the object that cross that line in the 3D are projected to single x coordinate in the player screen...
So if we are looking at some direction (red) any object/point crossing/touching this red line will be place at the center of screen (in x axis). What is left from it will be rendered on the left and similarly whats on right will be rendered on the right too...
With perspective we need to define how large viewing angle we got...
This limits our view so any point touches the green line will be projected on the edge of view (in x axis). From this we can compute screen x coordinate sx of any point (x,y,z) directly:
// angle of point relative to player direction
sx = point_ang - pangle;
if (sx<-M_PI) sx+=2.0*M_PI;
if (sx>+M_PI) sx-=2.0*M_PI;
// scale to pixels
sx = screen_size_x/2 + sx*screen_size_x/FOVx
where screen_size_x is resolution of our view area and point ang is angle of point x,y,z relative to origin px,py,pz. You can compute it like this:
point_ang = atan2(y-py,x-px)
but if you truly do a DOOM ray-casting then you already got this angle.
Now we need to compute the screen y coordinate sy which is dependent on the distance from player and wall size. We can exploit triangle similarity.
so:
sy = screen_size_y/2 (+/-) wall_height*focal_length/distance
Where focal length is the distance at which wall with 100% height will cover exactly the whole screen in y axis. As you can see we dividing by distance which might be zero. Such state must be avoided so you need to make sure your rays will be evaluated at the next cell if standing directly on cell boundary. Also we need to select the focal length so square wall will be projected as square.
Here a piece of code from mine Doom engine (putted all together):
double divide(double x,double y)
{
if ((y>=-1e-30)&&(y<=+1e-30)) return 0.0;
return x/y;
}
bool Doom3D::cell2screen(int &sx,int &sy,double x,double y,double z)
{
double a,l;
// x,y relative to player
x-=plrx;
y-=plry;
// convert z from [cell] to units
z*=_Doom3D_cell_size;
// angle -> sx
a=atan2(y,x)-plra;
if (a<-pi) a+=pi2;
if (a>+pi) a-=pi2;
sx=double(sxs2)*(1.0+(2.0*a/view_ang));
// perpendicular distance -> sy
l=sqrt((x*x)+(y*y))*cos(a);
sy=sys2+divide((double(plrz+_Doom3D_cell_size)-z-z)*wall,l);
// in front of player?
return (fabs(a)<=0.5*pi);
}
where:
_Doom3D_cell_size=100; // [units] cell cube size
view_ang=60.0*deg; // FOVx
focus=0.25; // [cells] view focal length (uncorrected)
wall=double(sxs)*(1.25+(0.288*a)+(2.04*a*a))*focus/double(_Doom3D_cell_size); // [px] projected wall size ratio size = height*wall/distance
sxs,sys = screen resolution
sxs2,sys2 = screen half resolution
pi=M_PI, pi2=2.0*M_PI
Do not forget to use perpendicular distances (multiplied by cos(a) as I did) otherwise serious fish-eye effect will occur. For more info see:
Ray Casting with different height size

Computing diameter-lines of a 3D spherical mask

Background
For an algorithm I'm working on, I currently use a 3D sphere as binary mask, with a NxNxN array having voxels in a sphere of radius N//2 as True. Further processing does computation for each voxel set as True.
It proved computationally intensive for my specific task as N grew large = O(N^3), so I now want to reduce my binary mask to a subsample of lines radiating from array center within radius.
Objective
I want a 3D binary mask of the lines in gray in the image.
To have a bit of control over the number of voxels, I would have a parameter (say l) regulating the number of lines sampled in each 2D circle, and maybe a second one (k ?) for the number of z-rotation.
What I tried
I am using numpy and scipy, and I thought that I could use the scipy.ndimage.interpolation.rotate method to rotate a single line around on a plane, then use that complete 2D mask to rotate around the z-axis.
This proved difficult, as interpolate uses some deep magic regarding splines that discard my True values on rotation.
I am thinking that I could compute mathematically which voxel should be set to True by following some line-equations, but I'm at a loss to find them.
Any idea how to get there ?
Update : Solution !
Thanks to jkalden who helped me think this through and gave code samples, I have this :
rmax is radius of sphere, n_theta and n_phi the number of polar and azimutal lines to use.
out_mask = np.zeros((rmax*2,) * 3, dtype=bool)
# for each phi = one circle in azimutal circles
for phi in np.linspace(0, np.deg2rad(360), n_phi,endpoint=False):
# for all lines in polar circle of this azimutal circle
for theta in np.linspace(0, np.deg2rad(360), n_theta,endpoint=False):
# for all distances (0-rmax) in these lines
for r in range(rmax):
coords = spherical_to_cartesian([r, theta, phi]) + rmax
out_mask[tuple(coords)] = True
With the spherical_to_cartesian from this code sample.
Which gives me this (with rmax = 50 and n_theta = n_phi = 8) :
(Center area tuned out of my function by choice)
I propose to change the coordinate system to spherical coordinates. Thus, you will choose your 2D circle by an azimuthal angle, and a line then is defined by additionally choosing a polar angle. The variable along the line is then just the radius, and you can use ´numpy.linspace´ to discretize it. Doing so might also save time during calculation.
You can switch your coordinate system any time by using the bijective relation which is implemented e.g. here or here.

Draw ellipses around points

I'm trying to draw ellipses around points of a group on a graph, with matplotlib. I would like to obtain something like this:
A dataset for a group (the red one for example) could look like this:
[[-23.88315146 -3.26328266] # first point
[-25.94906669 -1.47440904] # second point
[-26.52423229 -4.84947907]] # third point
I can easily draw the points on a graph, but I encounter problems to draw the ellipses.
The ellipses have diameters of 2 * standard deviation, and its center has the coordinates (x_mean, y_mean). The width of one ellipse equals the x standard deviation * 2. Its height equals the y standard deviation * 2.
However, I don't know how to calculate the angle of the ellipses (you can see on the picture the ellipses are not perfectly vertical).
Do you have an idea about how to do that ?
Note:
This question is a simplification of LDA problem (Linear Discriminant Analysis). I'm trying to simplify the problem to its most basic expression.
This is a well-studied problem. First take the convex hull of the set of points
you wish to enclose. Then perform computations as described in the literature.
I provide two sources below.
"Smallest Enclosing Ellipses--An Exact and Generic Implementation in C++" (abstract link).
Charles F. Van Loan. "Using the Ellipse to Fit and Enclose Data Points."
(PDF download).
This has a lot more to do with mathematics than programming ;)
Since you already have the dimensions and only want to find the angle, here is what I would do (based on my instinct):
Try to find the line that best fits the given set of points (trendline), this is also called Linear Regression. There are several methods to do this but the Least Squares method is a relatively easy one (see below).
Once you found the best fitting line, you could use the slope as your angle.
Least Squares Linear Regression
The least squares linear regression method is used to find the slope of the trendline, exactly what we want.
Here is a video explaining how it works
Let's assume you have a data set: data = [(x1, y1), (x2, y2), ...]
Using the least square method, your slope would be:
# I see in your example that you already have x_mean and y_mean
# No need to calculate them again, skip the two following lines
# and use your values in the rest of the example
avg_x = sum(element[0] for element in data)/len(data)
avg_y = sum(element[1] for element in data)/len(data)
x_diff = [element[0] - avg_x for element in data]
y_diff = [element[1] - avg_y for element in data]
x_diff_squared = [element**2 for element in x_diff]
slope = sum(x * y for x,y in zip(x_diff, y_diff)) / sum(x_diff_squared)
Once you have that, you are almost done. The slope is equal to the tangent of the angle slope = tan(angle)
Use python's math module angle = math.atan(slope) this will return the angle in radians. If you want it in degrees you have to convert it using math.degrees(angle)
Combine this with the dimensions and position you already have and you got yourself an ellipse ;)
This is how I would solve this particular problem, but there are probably a thousand different methods that would have worked too
and may eventually be better (and more complex) than what I propose.
I wrote a simple function to implement Mathieu David's solution. I'm sure there are many ways to do this, but this worked for my application.
def get_ellipse_params(self, points):
''' Calculate the parameters needed to graph an ellipse around a cluster of points in 2D.
Calculate the height, width and angle of an ellipse to enclose the points in a cluster.
Calculate the width by finding the maximum distance between the x-coordinates of points
in the cluster, and the height by finding the maximum distance between the y-coordinates
in the cluster. Multiple both by a scale factor to give padding around the points when
constructing the ellipse. Calculate the angle by taking the inverse tangent of the
gradient of the regression line. Note that tangent solutions repeat every 180 degrees,
and so to ensure the correct solution has been found for plotting, add a correction
factor of +/- 90 degrees if the magnitude of the angle exceeds 45 degrees.
Args:
points (ndarray): The points in a cluster to enclose with an ellipse, containing n
ndarray elements representing each point, each with d elements
representing the coordinates for the point.
Returns:
width (float): The width of the ellipse.
height (float): The height of the ellipse.
angle (float): The angle of the ellipse in degrees.
'''
if points.ndim == 1:
width, height, angle = 0.1, 0.1, 0
return width, height, angle
else:
SCALE = 2.5
width = np.amax(points[:,0]) - np.amin(points[:,0])
height = np.amax(points[:,1]) - np.amin(points[:,1])
# Calculate angle
x_reg, y_reg = [[p[0]] for p in points], [[p[1]] for p in points]
grad = LinearRegression().fit(x_reg, y_reg).coef_[0][0]
angle = np.degrees(np.arctan(grad))
# Account for multiple solutions of arctan
if angle < -45: angle += 90
elif angle > 45: angle -= 90
return width*SCALE, height*SCALE, angle

Categories