<jupyter_start><jupyter_text>This tutorial introduces numpy, a Python library for performing numerical computations in Python  In order to be able to use numpy we need to import the library using the special word `import`. Also, to avoid typing `numpy` every time we want to use one if its functions we can provide an alias using the special word `as`:<jupyter_code>import numpy as np<jupyter_output><empty_output><jupyter_text>Now, we have access to all the functions available in `numpy` by typing `np.name_of_function`. For example, the equivalent of `1 + 1` in Python can be done in `numpy`:<jupyter_code>np.add(1,1)<jupyter_output><empty_output><jupyter_text>Although this might not seem very useful, however, even simple operations like this one, can be much quicker in `numpy` than in standard Python when using lots of numbers.  To access the documentation explaining how a function is used, its input parameters and output format we can press `Shift+Tab` after the function name"<jupyter_code>np.add<jupyter_output><empty_output><jupyter_text>By default the result of a function or operation is shown underneath the cell containing the code. If we want to reuse this result for a later operation we can assign it to a variable:<jupyter_code>a = np.add(2,3)<jupyter_output><empty_output><jupyter_text>The contents of this variable can be displayed at any moment by typing the variable name in a new cell:<jupyter_code>a<jupyter_output><empty_output><jupyter_text>The core concept in numpy is the `array` which is equivalent to lists of numbers but can be multidimensional. To declare a numpy array we do:<jupyter_code>np.array([1,2,3,4,5,6,7,8,9])<jupyter_output><empty_output><jupyter_text>Most of the functions and operations defined in numpy can be applied to arrays. For example, with the previous operation:<jupyter_code>arr1 = np.array([1,2,3,4])
arr2 = np.array([3,4,5,6])

np.add(arr1, arr2)<jupyter_output><empty_output><jupyter_text>But a more simple and convenient notation can also be used:<jupyter_code>arr1 + arr2<jupyter_output><empty_output><jupyter_text>Arrays can be sliced and diced. We can get subsets of the arrays using the indexing notation which is `[start:end:stride]`. Let's see what this means:<jupyter_code>arr = np.array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])

print(arr[5])
print(arr[5:])
print(arr[:5])
print(arr[::2])<jupyter_output><empty_output><jupyter_text>Experiment playing with the indexes to understand the meaning of start, end and stride. What happend if you don't specify a start? What value numpy uses instead? Note that numpy indexes start on `0`, the same convention used in Python lists.  Indexes can also be negative, meaning that you start counting by the end. For example, to select the last 2 elements in an array we can do:<jupyter_code>arr[-2:]<jupyter_output><empty_output><jupyter_text>Can you figure out how to select all the elements in the previous array excluding the last one, [15]?  What about doing the same but now every 3rd element? Hint: Result should be ?`[0,3,6,9,12]`  Numpy arrays can have multiple dimensions. For example, we define a 2-dimensional `(1,9)` array using nested square bracket:<jupyter_code>np.array([[1,2,3,4,5,6,7,8,9]])<jupyter_output><empty_output><jupyter_text>To visualise the shape or dimensions of a numpy array we can add the suffix `.shape`<jupyter_code>print(np.array([1,2,3,4,5,6,7,8,9]).shape)
print(np.array([[1,2,3,4,5,6,7,8,9]]).shape)
print(np.array([[1],[2],[3],[4],[5],[6],[7],[8],[9]]).shape)<jupyter_output><empty_output><jupyter_text>Any array can be reshaped into different shapes using the function `reshape`:<jupyter_code>np.array([1,2,3,4,5,6,7,8]).reshape((2,4))<jupyter_output><empty_output><jupyter_text>If you are concerned about having to type so many squared brackets, there are more simple and convenient ways of doing the same:<jupyter_code>print(np.array([1,2,3,4,5,6,7,8,9]).reshape(1,9).shape)
print(np.array([1,2,3,4,5,6,7,8,9]).reshape(9,1).shape)
print(np.array([1,2,3,4,5,6,7,8,9]).reshape(3,3).shape)<jupyter_output><empty_output><jupyter_text>Also there are shortcuts for declaring common arrays without having to type all their elements:<jupyter_code>print(np.arange(9))
print(np.ones((3,3)))
print(np.zeros((2,2,2)))<jupyter_output><empty_output><jupyter_text>Can you try to declare a 3-dimensional array of shape (5,3,3)? Assign it to a variable  Create another one with the same shape and use the numpy function to add both arrays:  Some useful functions in Numpy for calculating the mean, standard deviation and sum of the elements of an array. These operation can be performed only in certain axis.<jupyter_code>arr = np.arange(9).reshape((3,3))

print(arr)

print(np.mean(arr))
print(np.std(arr))

print(np.mean(arr, axis=0))
print(np.mean(arr, axis=1))

print(np.sum(arr))<jupyter_output><empty_output><jupyter_text>Numpy data types Numpy arrays can contain numerical values of different types. These types can be divided in these groups: * Integers    * Unsigned        * 8 bits: `uint8`        * 16 bits: `uint16`        * 32 bits: `uint32`        * 64 bits: `uint64`    * Signed        * 8 bits: `int8`        * 16 bits: `int16`        * 32 bits: `int32`        * 64 bits: `int64`* Floats    * 32 bits: `float32`    * 64 bits: `float64`     We can specify the type of an array when we declare it or change the type of an existing one with the following expressions:<jupyter_code>arr = np.ones((10,10,10), dtype=np.uint8)

arr[4,4,4] = -1
print(arr[4,4,4])

arr = arr.astype(np.int8)
print(arr[4,4,4])

arr = arr.astype(np.float32)
print(arr[4,4,4])<jupyter_output><empty_output><jupyter_text>Broadcasting Numpy is set up internally in a way that arrays involved in operations are promoted to match the shapes<jupyter_code>a = np.zeros((10,10))

a += 1

a
a = np.arange(9).reshape((3,3))

b = np.arange(3)

a + b<jupyter_output><empty_output><jupyter_text>Booleans There is a binary type in numpy called boolean which encodes `True` and `False` values. For example:<jupyter_code>arr = (arr > 0)

print(arr[:,:,4])

arr.dtype<jupyter_output><empty_output><jupyter_text>Boolean types are quite handy for indexing and selecting parts of images as we will see later. Many numpy functions also work with Boolean types.<jupyter_code>print(np.count_nonzero(arr[:,:,4]))

a = np.array([1,1,0,0], dtype=np.bool)
b = np.array([1,0,0,1], dtype=np.bool)
np.logical_and(a, b)<jupyter_output><empty_output><jupyter_text>Depending of the language that you have used before this behaviour in Python might strike you:<jupyter_code>a = np.array([0,0,0])

# We make a copy of array a with name b
b = a

# We modify the first element of b
b[0] = 1

print(a)
print(b)<jupyter_output><empty_output><jupyter_text>Both arrays have been modified. This is in fact because a and b are references to the same array. If you want to have variables with independent arrays you'll have to use the `b = np.copy(a)` function.  This second part introduces matplotlib, a Python library for plotting numpy arrays as images.  For the purposes of this tutorial we are going to use a part of matplotlib called pyplot. We import it by doing:<jupyter_code>%matplotlib inline

import numpy as np
from matplotlib import pyplot as plt<jupyter_output><empty_output><jupyter_text>An image can be seen as a 2-dimensional array. To visualise the contents of a numpy array:<jupyter_code>arr = np.arange(100).reshape(10,10)

print(arr)

plt.imshow(arr)<jupyter_output><empty_output><jupyter_text>Can you create a similar image with an array with shape (50,50)?  We can use the Pyplot library to load an image using the function `imread`<jupyter_code>im = np.copy(plt.imread('data/black_mountain_fire.jpg'))<jupyter_output><empty_output><jupyter_text>This image is a 3-dimensional numpy array. By convention the first dimension corresponds to the vertical axis, the second to the horizontal axis and the third are the Red, Green and Blue channels of the image. What are the dimensions of the `im` array? __Hint: Use the `.shape` property of the im variable.  Let's display this image using the `imshow` function.<jupyter_code>plt.imshow(im)<jupyter_output><empty_output><jupyter_text>This is a photo of Black Mountain taken during prescribed burns in 2014. A colour image is normally composed of three layers containing the values of the red, green and blue pixels. When we display an image we see all three colours combined.  Knowing the extents of the image given by its shape, can you display the values of one of the pixels in the sky? You need to provide one index for each x and y dimensions and get all three channels. Make sure the values represent a mostly blue pixel.  Let's use the indexing functionality of numpy to select a slice of this image. For example to select the top right corner:<jupyter_code>plt.imshow(im[:800,-800:,:])<jupyter_output><empty_output><jupyter_text>Let's practice your indexing skills! Can you create a cropped image around Black Mountain's tower? Remember: first dimension is the vertical coordinates, second dimension is the horizontal coordinates and the third are the RGB channels of the image.  Let's play around with this a little bit. For example, let's replace all the values in the 'red' layer with the value 255, this is the highest red value possible and it will make your whole image redish. The following command will replace all the values in the red channel (axis 3) with the value 255, and see what happens<jupyter_code>im[:,:,0] = 255
plt.imshow(im)<jupyter_output><empty_output>