3

I have an equation that creates an ellipse in the general form x^2/a^2 + y^2/b^2 = 1. I wish to produce an array whereby all points inside the ellipse are set to one and all points outside are a zero. This array is then to be convolved with another.

So far I have tried to create an empty array of the size I want the go through all x,y positions calculating x^2/a^2 + y^2/b^2 = 1. If the general form is less than one enter a one into the array, otherwise continue to the next x,y position.

Here is my code:

    arr = numpy.array(im) 
    sh = numpy.shape(arr)
    ar = numpy.empty(sh)

    for x in range (sh[0]):
        xx = x*x
        for y in range (sh[1]):
            yy = y*y
            ellips = xx/(a*a)+yy/(b*b)
            if ellips < 1:
                ar[xx,yy] = '1'
            else:
                break

However, this doesn't produce what I expect as my ellipse is always centered at (0,0) thus I expect the ones to be in the center of my array but they appear in the top left corner.

Does anyone have insight as to where I have gone wrong? Or perhaps a better way to produce my array?

---Edit---

Having tried EOL's answer I receive an ellipse array however it doesn't match the ellipse it should model. Here is a picture to illustrate what I mean: https://i.stack.imgur.com/aVx4I.jpg The ellipse array doesn't have the rotation of the ellipse. The code to produce the ellipse and ellipse array is as follows:

    def Ellipssee(z,stigx,stigy):
        points=100 #Number of points to construct the ellipse
        x0,y0 = 0,0 #Beam is always centred
        z0 = 4 # z0 a constant of the device
        al = 2 # alpha a constant of the device
        de = sqrt((stigx**2 + stigy**2))
        ang = arctan2(stigy, stigx) # result in radians
        a = (z+de-z0)*al
        b = (z-de-z0)*al 
        cos_a,sin_a=cos(ang),sin(ang)
        the=linspace(0,2*pi,points)
        X=a*cos(the)*cos_a-sin_a*b*sin(the)+x0
        Y=a*cos(the)*sin_a+cos_a*b*sin(the)+y0

        img = Image.open("bug.png").convert("L") # load image for array size
        arr = np.array(img) 
        sh = np.shape(arr)

        nx = sh[0]   # number of pixels in x-dir
        ny = sh[1]   # number of pixels in y-dir

        x0 = 0;  # x center, half width                                       
        y0 = 0;  # y center, half height                                      
        x = np.linspace(-60, 60, nx)  # x values of interest
        y = np.linspace(-30, 30, ny)  # y values of interest
        ellipseArr = ((x-x0)/a)**2 + ((y[:,None]-y0)/b)**2 <= 1

I have been calling the method with the values Ellipse(1,6,8).

Why is the rotation being lost in the array creation?

mysterious-bob
  • 105
  • 2
  • 10
  • If you want the ellipse to be centered somewhere else than in (0, 0)—the "top left corner"—, say in (x0, y0), then you should use `ellips = ((x-x0)/a)**2 + ((y-y0)/b)**2`. – Eric O. Lebigot Jul 31 '14 at 04:37
  • @EOL thanks for your comment. I don't want my ellipse anywhere other than the center (0,0), so there is no need to include an x0,y0 term in the general equation. Edit* Having read the answer from Gabriel I understand what you mean now. Thanks for pointing that out :) – mysterious-bob Jul 31 '14 at 04:54
  • 1
    EOL is right Nate. You have to either adjust your equation, or use a coordinate system centered on (0,0). see my answer for the details – Gabriel Jul 31 '14 at 04:55
  • also, as your original code is, you don't want to use `break` you want to set `ar[xx,yy] = '0' where `break` is. `np.empty` creates an array without initializing any values so they need to be set to '0' if they don't satisfy the inequality. – Gabriel Jul 31 '14 at 04:59
  • @Nate: the problem with the coordinates is that arrays "start" at (0, 0)—this is one of their corners. This is why if you center your ellipse in (0, 0), you are centering it on a corner—hence the need for an ellipse centered somewhere else. – Eric O. Lebigot Jul 31 '14 at 05:28

2 Answers2

7

NumPy can do this directly, with no Python loop:

>>> import numpy as np
>>> from matplotlib import pyplot

>>> x0 = 4; a = 5  # x center, half width                                       
>>> y0 = 2; b = 3  # y center, half height                                      
>>> x = np.linspace(-10, 10, 100)  # x values of interest
>>> y = np.linspace(-5, 5, 100)[:,None]  # y values of interest, as a "column" array
>>> ellipse = ((x-x0)/a)**2 + ((y-y0)/b)**2 <= 1  # True for points inside the ellipse

>>> pyplot.imshow(ellipse, extent=(x[0], x[-1], y[0], y[-1]), origin="lower")  # Plot

enter image description here

A few key points of this technique are:

  • The squares (in x and y) are calculated only once (and not for each point individually). This is better than using numpy.meshgrid().

  • Thanks to NumPy's broadcasting rules, the contributions of x and y are summed together in a simple way (y[:,None] essentially makes y a column vector of y value, while x remain a row vector). There is also no need for larger 2D intermediate array, as would be needed with numpy.meshgrid().

  • NumPy can do the "is in ellipse?" test directly (that's the <= 1 part), with no Python loop.

Now, if you can live with integer coordinates only, x and y can simply be obtained with np.arange() instead of the more "refined" np.linspace().

While ellipse is an array of booleans (in/out of the ellipse), False is treated like 0 and True like 1 in calculations (for instance ellipse*10 produces an array of 0 and 10 values), so you can use it in your NumPy calculations.

Eric O. Lebigot
  • 91,433
  • 48
  • 218
  • 260
  • I have had a play with your code however, my ellipse array doesn't match my ellipse. I shall edit my question to explain what I mean. Also, thank you for your discussion of the other approach and explaining its short comings. – mysterious-bob Jul 31 '14 at 06:09
  • Equation `((x-x0)/a)**2 + ((y-y0)/b)**2` is definitely not the general form of an ellipse: it's an ellipse whose axes are vertical and horizontal. If you want a more general ellipse, you simply have to use the more general equation in `ellipse = …`. I modified the code a bit so that you can directly use the correct ellipse equation in the expression for `ellipse`. You need to figure out what the correct equation is, though (the thing to keep in mind is that in your question `X` and `x` do not represent the same quantity; you want to express `X` (and `Y`) as a function of `x` and `y` first). – Eric O. Lebigot Jul 31 '14 at 08:32
  • Woops!! You are quite correct. My general equation has `X=xcos(the)-sin(the)` & `Y=xsin(the)+ycos(the)` in order to accommodate the angle. I didn't look close enough at my scribbles! Thank you for your help – mysterious-bob Jul 31 '14 at 08:55
1

Here is an answer using some tricks from numpy.

import numpy as np

a = 3.0
b = 1.0
nx = 256   # number of pixels in x-dir
ny = 256   # number of pixels in y-dir

# set up a coordinate system
x = np.linspace(-5.0, 5.0, nx)
y = np.linspace(-5.0, 5.0, ny)

# Setup arrays which just list the x and y coordinates
xgrid, ygrid = np.meshgrid(x, y)

# Calculate the ellipse values all at once
ellipse = xgrid**2 / a**2 + ygrid**2 / b**2

# Create an array of int32 zeros
grey = np.zeros((nx,ny), dtype=np.int32)

# Put 1's where ellipse is less than 1.0
# Note ellipse <1.0 produces a boolean array that then indexes grey
grey[ellipse < 1.0] = 1

Note that I've used coordinates xgrid and ygrid as opposed to pixel numbers as you did in your OP. Because my coordinate system is centered on 0.0,0.0, I get an ellipse in the middle of my array. In your question you used pixel indices which have 0,0 in the corner so you get an ellipse in the corner. You can either move your coordinate system (as I did) or account for the shift in your ellipse equation (as the comment suggested).

Eric O. Lebigot
  • 91,433
  • 48
  • 218
  • 260
Gabriel
  • 10,524
  • 1
  • 23
  • 28
  • This is less memory and computation efficient than my answer: this is a solution which is less general (when memory or computation time are an important criterion). In fact, `meshgrid()` creates full 2D arrays when they are not needed (only the values of the ellipse function need to be a 2D array). Furthermore, calculating `xgrid**2` is more expensive to calculate than `x**2`, since it is `nx` (256!) times bigger. Finally, there is a priori no need to return an array of `int32`, and `grey = ellips < 1` would do (no need for `1.0` either: NumPy knows its number conversions). – Eric O. Lebigot Jul 31 '14 at 05:13
  • Now, concentrating on timing only (not memory), I just did a test (with `nx = ny = 100`): `meshgrid()`+`xgrid**2+ygrid**2` is more than *4 times slower* than `x**2 + y[:,None]**2`—in addition to using 3 times more memory. So, in this case, the `mesgrid()` machinery requires more steps (construction of `xgrid` and `ygrid`), more memory and more calculation time (with things getting worse and worse with more points). – Eric O. Lebigot Jul 31 '14 at 05:19
  • Broadcasting rules are great, but sometimes clarity trumps efficiency. If you need to generate large arrays or do this operation many times, by all means, go for the broadcasting solution. It's slick and fast. However, you can still generate an image with more than enough pixels for a 1080p monitor (nx=ny=2048) in a fraction of a second with this method and it's easier to read. – Gabriel Jul 31 '14 at 05:58
  • I agree that not having to think about broadcasting makes things easier to understand. I agree about the display use too. However, there are other uses around than displaying the result (for examples when many 2D functions must be calculated in a loop, for instance for optimizing some parameters, etc.). So, yeah, there is a meaningful choice between legibility and speed, here. I still don't see any reason to not do `grey = ellips < 1`. No downvote from me, though. – Eric O. Lebigot Jul 31 '14 at 08:39