Hi guys, welcome back to CS445 - Computational Photography.
Today I'm going to be briefly going over some basics of how to use NumPy, an essential python library for scientific computing.
You'll be using NumPy a lot in this class, and it is pretty useful for anything you might do in data science, computer vision, and
anything that involves matrices and arrays. The best resource for learning how to use NumPy is the NumPy documentation. The link is right here.
As always you'll receive this in the course material. This notebook and any other notebooks we use in these tutorials.
With that, let's get started. You can go to numpy.org/#getting-started, and there are instructions on how to download and install NumPy.
So I've done so here. Then we'll open up a Jupyter notebook and we'll import numpy. [Note: If you do "import numpy as np" then you should refer to numpy as "np" in your code.]
Alright so the fundamental objects in NumPy are called arrays. If you've programmed in C or C++,
they're pretty much what you are used to in those languages.
Actually, more specifically, NumPy arrays are quite similar to Matlab arrays, and a lot of interfaces are pretty much the same.
Some of the important attributes of NumPy arrays are the shape and the data type. You're going to specify these when you create an array
and you're going to use the shape when you loop through the array or utilize its properties.
The shape is a tuple whose length is the number of dimensions. The data type refers to the kinds of objects that are in the NumPy array.
They could be ints, floats or even strings. Let's go over some ways to create arrays. NumPy's ndarray class has a low-level constructor, but we rarely use it.
Generally what we do is we use a standard python list. So as you can see, "seq" is a list here.
Then we use the np.array function which takes another type of sequence and converts it into a NumPy array. So let's run this cell.
There you go, as you can see the type of "arr_seq" is numpy.ndarray, and its data type is int64. This is the default integer data type that NumPy parses.
You can look more closely at the defaults here at this website, the scipy docs for NumPy - NumPy ndarray class.
You can google for the numpy.array functions. The resources are really rich.
So alternatively, NumPy provides utility functions for common array creation use cases. Sometimes you just want to create an array that may be empty or look a certain way.
One way to do this is through these functions. So let's run numpy.empty. numpy.empty is going take a tuple of the shape that you want your array to have.
So we just want a 2-by-2 empty array here. As you can see the array is not strictly empty but it is uninitialized, and that's the point of np.empty.
It's a very fast function as it doesn't initialize any of the memory locations that it uses. Yeah that's np.empty.
There is also np.ones. So most of these functions take a given shape, as you can see here, so you have to provide them with a tuple of the size and shape that you want your array to be.
Here we go, with np.ones, we want a 1-by-4 array, that's 1 row and 4 columns. This is what that looks like.
Then running np.zeros with a 4-row 1-column array will give us this. So np.ones just fills up the array with all 1 and np.zeros fills up the array with all 0.
Then there's the general version of np.ones and np.zeros called np.full which takes at least two arguments.
The first argument is the shape, as usual. The second argument is the number that you want to fill the array with.
Here I've asked for a 4-element array filled with 10. Notice one important difference between this shape (4,) and (4,1).
So these are strictly not the same. The (4,1) array is a 2-dimensional array. So you can see that there are two different sets of square brackets. There is one outside and then there is this set encapsulating the inner elements.
Here we have 4 rows and 1 column per row, whereas here we just have a single 1-dimensional array with 4 elements.
NumPy has support for 1-dimensional arrays, so this is not the same as (1,4) or (4,1), and we'll talk a little bit more about this with regards to reshaping and broadcasting.
Another useful function is called numpy.arange, which basically just gives you evenly spaced values within a given interval. I'm going to be using this function quite a bit so I thought I'd cover it.
This function is especially useful when you're plotting. It will come in handy, so make sure you know how to use this one.
Now let's talk about how to access arrays. NumPy array elements are accessed using the standard python access syntax. It's the obj, in this case the array, and inside square brackets it's going to have some selector.
As with multi-dimensional python lists, you can use commas to separate the selectors for various dimensions and this colon you can use to select ranges within one dimension.
Let's take a look at this we'll make "x" a range from 0 to 20 with a step of 2. So this is going to generate the even numbers between 0 and 20 not including 20. That's actually important to note here. So let's run that.
Then we're going to have x[2] give us 4, and the way to read this is x[1:3] will give us 2 and 4. The 0th element is 0. The 1st element is 2.
The second element is 4. So notice that you actually don't get x[3] here. That's important to note. Let's actually see what x[3] is here. It's 6. So x[1:3] actually doesn't include x[3] and that's important to know.
I encourage you try more examples, obviously. You might want to try with negative indices as ranges in python do support negative indices as well negative steps.
Here maybe we can add a step of 1 or of 2. That will give you a slightly smaller array. But yeah I encourage you to mess around with that.
Let's quickly talk about the memory layout of arrays. All arrays in NumPy are 1-dimensional contiguous segments.
There is no 2-dimensional arrays in memory. The only thing that is allowing you to interface with them two dimensionally is an indexing scheme that maps the total number of integers in that array into certain indices that's managed by numpy.
This indexing scheme is dependent on the shape, and how many bytes each item takes which is determined by the dtype associated with the array.
It is also important to note that data in NumPy arrays is arranged in row-major order which means that if you have a 2-dimensional array, the first dimension is going to be the row.
So the way to read this is going to be the first row (the index 0 row) and the third column (the index 2 column).
Let's just see what that's about. We've created an empty 3-by-3 array. We're going to access the first row, last column here. There we have it 2.058e-312 that's this element. So that's row-major.
Now let' s talk a liitle about reshaping and resizing. There are two important array functions used to manipulate the shape of arrays. We mainly use reshape and I think you'll probably be mainly using reshape as well in this course.
Reshape returns an array with the same data as the given array, but with the shape that you provide. So let's look at some examples. Let's create a 1-dimensional array of size 12.
There it is. Now we'll reshape it to be a 2-dimensional array with 2 rows and 6 columns. That's what it's going to look like. Let's see, we can also reshape to something else.
I think it's important to note here that you can pass in a tuple as well for reshape, but it's not necessary, you can also pass in the numbers directly as a sequence of dimensions.
So I can do this, and turn it into a 3-dimensional array with 12 rows, 1 column and 1 cell deep. So feel free to mess around with that. For now we'll reshape it to (3,4) that looks like this.
Note that what resize does is a little different. It changes the shape of the given array in place. So at this point after doing x.reshape(3,4), we haven't actually modified "x" in any way. The output of this expression is a 2-dimensional array of shape 3-by-4.
However, once we execute resize, let's see what happens. Let's get rid of this for a second. So resize doesn't produce any output but it does modify the array, so let's query what happened to our array.
So there it is. Let's try this one more time. We have "x" here. It is an array of 12 elements. Before doing resize, let's check out what happened to "x".
So "x" is like this. No we'll do resize and query "x" again.
So this is the same as the output of x.reshape, but now we've modified "x".
Well that was reshaping and resizing. I encourage you to try multiple examples. There are some caveats that arise when you are trying to
reshape the channels of images, you have to be really specifically careful with the order in which you reshape. You might need to use some reshapes and transposes. When those situations arise in this class, we will be specific about them.
Next let's talk about array slicing and indexing. Like we saw in the array access section, arrays are accessed using the standard python bracket syntax.
So in this section we'll look at various forms of the selection object i.e. what you can put inside the square brackets.
So one thing you can put inside the square brackets are ranges. The basic range selection syntax is start:stop:step. So let's look at an example of this.
So here is a 1-dimensional array of 100 elements reshaped to a 2-dimensional 10-by-10 array. The way we would select the bottom right 5-by-5 subarray.
So how would we do that? We want to start at the 5th row and the 5th column and select everything to the bottom and right of that.
When you don't specify a stop value like this, NumPy assumes that you mean you want to iterate till the end. So it will replace this with 10 implicitly in this case.
The default value for the step is 1. Here we're messing around with what the value of the step can be. So we can even have it be negative values.
So if we put -1, what we'll get is, starting at the 5th row 5th column, we'll get the entire 5-by-5 subarray to the top-left of that.
But, it'll be rotated 180 degrees. So this is a neat way to rotate a subarray by 180 degrees. Just change the step to -1. I'd definitely give this a shot.
Another thing we can do is to take the whole array and change the step that we use. That way we can choose various elements from the subarray, and do some sort of downsampling.
So definitely experiment with these values and as always, if you have any questions about this, do let us know on Piazza.
Let's talk about some more objects we can put inside the square brackets. The three dots or ellipses object along with the new axis object can also be used for indexing.
What the new axis and ellipses objects are for is: the ellipses object (...) is for substituting all of the first few dimensions.
A specific use case might be like the one here. You want to view an array of all first elements along the third dimension in this array.
What that would mean is that you want to select... Essentially what you're doing is computing this (for a 3-dimensional array, x[...,0] is the same as x[:,:,0].). So what just the ":" means is "take everything" (in that dimension).
Assume the start, which is by default 0, and implicitly assume that you want to go all the way up to the end along the first and second dimensions.
Then just take the 0th element. So here for 3 dimensions we can do this, but if we had a 10-dimensional array, instead of putting 10 colons here, we can just replace all this with 3 dots.
That will give us what we want. Oops, we forgot to run this cell. There we go. So that's what the ellipses are for.
You can also use the ellipses to fill in the rest. So if you just want the first element along the first dimension, and everything in that element you can use the ellipses for that.
But this is equivalent to just this. (x[0,...] is the same as x[0].) So that's why it's not as useful as in the first case.
Next let's talk about the newaxis object. The newaxis object is used when you want to expand the shape of an array along a certain dimension. So let's look at "x" which has shape (3,3,3).
Now we'll select all the elements in "x" using just colons. So 3 colons means that we'll take all elements along all dimensions.
But at the second position, we'll insert an np.newaxis, and now we'll look at the shape of this. As you can see there is an extra dimension here and it has a depth of 1.
The np.newaxis is basically just an alias for the python None object, so if you had a None in here, it would basically have the same effect.
Using the np.newaxis object at the beginning without any other dimensions would prepend a dimension at the beginning of the shape.
You can use the ellipses and the np.newaxis object in tandem to easily append a dimension to the end of the shape, so this is the way you can expand it along the last position.
Now let's talk about advanced indexing. Advanced indexing is triggered when the selection object i.e. the object inside the square brackets is a non-tuple sequence object, another ndarray, or a tuple with at least one sequence object or ndarray.
There are specifically two types of advanced indexing: Integer and Boolean. It's important to note that unlike slicing, or basic indexing which returns a view of the underlying array data, advanced indexing always returns a copy of the data.
So a view is kind of like a pointer. It just shows you the data. When you print out the result of a basic slice, you are not copying any of the data. You don't have a copy of the data, but advanced indexing always returns a copy of the data.
So let's look at integer indexing first. We'll start with the same array we were working with earlier. What does this look like it will give us?
Each of these sequences of integers are of length 2, which means that we'll be selecting 2 items. This is basically specifying the value of one dimension for 2 elements.
It's the same with these two sequences. So basically we'll get the elements at (0,1,0) and (2,2,1). So let's run that.
Now let's change these numbers around a little bit.
So you can pause the video and think about what this will return. Let's run it. So we got back 21 and 9. So what happens when we change the length of these sequences?
Note that each one of these has to be of the same length. So you can't change the length of one of them and have the other ones not be changed. So here we're asking for 3 values from the array. So let's run that.
So that's how integer indexing works there. Now let's look at a different array. Let's look at a 4-by-4 array here. How exactly would we select just the corner elements of this array? That's 0, 3, 12, and 15.
So one way we can do it is with integer indexing. We'll define the corner rows as a 2-d array.
This is what it's going to look like. The corner columns are another 2D array. What NumPy lets us do here is just pass these 2D arrays to the square brackets. It'll give us the corner elements.
So how is this being interpreted here? Basically, NumPy will match these element wise or broadcast them, and we'll get the elements at those positions.
So here we'll get element (0,0), (0,3), (3,0) and (3,3) which are the corner elements in a 4-by-4 array.
So we can also achieve this with broadcasting. If we define the corner rows here like this and the corner columns like this as well, we can use np.newaxis to force a broadcast here.
What will happen is that NumPy knows that this becomes a 3-by-1 array and this is still a 1-dimensional array, it will broadcast them into these implicitly.
We'll once again get the corner elements just like we wanted. I encourage you mess around with these values and try this. This is yet another example of broadcasting.
The shape of this is 3-by-1 and here we have a just a (3,None) shape. So what would the output shape of this be?
It is a (3,3), so think about why that is.
Let's look at boolean indexing now. Arrays in NumPy are compatible with boolean operators and we can exploit this to essentially filter items in the array.
So let's look at this range from -10 to 10. If we want only the positive elements we can just pass in this expression into the square brackets.
That'll give us an array of all the positive values. Remember this is a copy. This is not referring to these elements in the array. This is a copy.
Similarly, we can find the even elements by passing in this selector. We can get the odd elements by just changing this.
Here we go, these are the odd elements. It should be noted that you can modify the values of the original array using these types of operators.
As shown here, what we want to do is increment all the negative values by 10. Let's go ahead and run this. This should give us no particular output,
but then if we query "x" again, we'll see that all the negative elements have been incremented by 10.
Well that was the NumPy tutorial for CS445. You should go through this one more time on your own, and feel free to ask questions on Piazza.
You'll be using NumPy extensively so this tutorial should come in handy. Okay, thank you!