<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Introdução ao NumPy
O tipo ndarray
O tipo ndarray, ou apenas array é um arranjo de itens homogêneos de dimensionalidade N, indexados por uma tupla de N inteiros. Existem 3 informações essenciais associadas ao ndarray
Step1:  Veja a seguir uma matriz bidimensional de dados ponto flutuante de 2 linhas e 3 colunas. Observe que a tupla do shape aumenta para a esquerda,
isto é, se eu tiver um vetor de 3 elementos, o seu shape é (3,) e se uma nova dimensão for adicionada, por exemplo 2 linhas e 3 colunas, o
shape passa a ser (3,2). O que antes shape[0] no vetor unidimensional era colunas, já na matriz bidimensional shape[0] passou a ser o número
de linhas.
Assim o último elemento da tupla do shape indica o número de colunas, a penúltima o número de linhas. Assim se quisermos sempre buscar
o número de colunas, independentemente do número de dimensões, shape[-1] informa sempre o número de colunas, shape[-2], o número de linhas.
Step2:  Manipulação de arrays
Criando arrays inicializados
É possível criar arrays de zeros, uns ou valores não inicializados usando as funções zeros, ones ou empty. As dimensões
do array são obrigatórias e é dado por uma tupla e o tipo dos elementos é opcional, sendo que o default é tipo float. 
O código a seguir cria 3 arrays. O primeiro possui 2 linhas e 4 colunas. O segundo tem 3 dimensões
Step3:  Note que o Numpy permite arrays n-dimensionais. Em imagens em níveis de cinza iremos trabalhar com matrizes bidimensionais mas
se a imagem for colorida, iremos representá-la em 3 canais, R, G e B, representados na estrutura com 3 dimensões. 
Se for um vídeo, isto é, uma sequência de imagens, teremos que adicionar mais uma dimensão. 
Se for uma tomografia, também podemos representar em 3 dimensões
Step4:  Veja que no último caso, usando o linspace, a sequência começa em 0 e termina em 2 e deve possuir 5 elementos. Veja que
para isto o passo a ser utilizado será 0.5, calculado automaticamente. Já no exemplo anterior, a sequência começa em 0 e deve terminar antes de 2 e o passo é 0.5. 
Fatiamento em narray unidimensional
Um recurso importante do numpy é o fatiamento no qual é possível acessar
um subconjunto do array de diversas formas. O fatiamento define os índices
onde o array será acessado definindo o ponto inicial, final e o passo entre
eles, nesta ordem
Step5:  Exemplo simples de fatiamento
Para a realização do fatiamento são utilizados 3 parâmetros, colocados no local do índice do array.
Os 3 parâmetros são separados por dois pontos "
Step6:  Exemplo de fatiamento com indices negativos
Acessando o último elemento com índice negativo
O código abaixo acessa os elementos ímpares até antes do último elemento
Step7:  Inversão do array com step negativo (step = -1)
Step8:  Fatiamento avançado
É possível realizar o fatiamento utilizando os 3 parâmetros explícitos
( o limite inferior, limite superior e o step), ou podemos suprimir algum 
desses parâmetros. Nestes casos a função toma o valor defaut
Step9:  Supressão do indice limite superior
Quando o índice do limite superior é omitido, fica implícito 
que é o último elemento
Step10:  Supressão do indice do step
O índice do step é opcional e quando não é indicado, seu valor é 1
Step11:  Todos os elementos com passo unitário
Step12:  Fatiamento no ndarray bidimensional
Um recurso importante do numpy é o fatiamento no qual é possível acessar
partes do array de diversas formas, como pode ser visto abaixo
Step13:  Fatiamento de linhas e colunas de um array
O operador 
Step14:  Fatiamento de elementos específicos de um array
Step15:  Fatiamento com índices invertidos
Step16:  Copiando variáveis ndarray
O ndarray foi projetado para acesso otimizado a uma grande quantidade de dados. Neste sentido, os conceitos
descritos a seguir sobre as três formas de cópias entre variáveis ditas sem cópia, cópia rasa (shallow) e
cópia profunda (deep) são fundamentais para uma codificação eficiente. Podemos dizer que um ndarray possui
o cabeçalho que contém dados pelas informações sobre o tipo do elemento, a dimensionalidade (shape) e 
passo ou deslocamento para o próximo elemento (strides) e os dados raster em si. A tabela
a seguir mostra a situação do cabeçalho e dos dados nos três tipos de cópias.
|Tipo                 | Cabeçalho
Step17:  Observe que mesmo no retorno de uma função, a cópia explícita pode não acontecer. Veja o exemplo a
seguir de uma função que apenas retorna a variável de entrada
Step18:  Cópia rasa
A cópia rasa é muito útil e extensivamente utilizada. É usada quando se quer indexar o array original 
através da mudança de dimensionalidade ou do 
refatiamento, porém sem a necessidade de realizar uma cópia dos dados raster. Desta forma consegue-se
uma otimização no acesso ao array n-dimensional. Existem várias formas onde a cópia rasa acontece,
sendo as principais
Step19:  Slice - Fatiamento
O exemplo a seguir mostra a cópia rasa no uso de fatiamento. No exemplo, todos os elementos de linhas
e colunas pares são modificados para 1. CUIDADO
Step20:  Este outro exemplo é uma forma atraente de processar uma coluna de uma matriz bidimensional,
porém é preciso CUIDADO, pois o uso de b deve ser com b[
Step21:  Transposto
A operação matricial de transposição que troca linhas por colunas produz também um view 
da imagem, sem necessidade de cópia
Step22:  Ravel
Aplicando-se o método ravel() a um ndarray, gera-se um view do raster
linearizado (i.e. uma única dimensão) do ndarray.
Step23:  Cópia profunda
Cria uma copia completa do array, do seu shape e conteúdo. A recomendação é utilizar a 
função copy() para realizar a copia profunda, entretanto é possível conseguir a
copia profunda pelo np.array.
Step24:  Operações matriciais
Uma das principais vantagens da estrutura ndarray é sua habilidade de processamento matricial.
Assim, para se multiplicar todos os elementos de um array por um escalar basta escrever a * 5 por
exemplo. Para se fazer qualquer operação lógica ou aritmética entre arrays, basta escrever a <oper> b
Step25:  Multiplicação de array por escalar
Step26:  Soma de arrays
Step27:  Transposta de uma matriz
Step28:  Multiplicação de matrizes
Step29:  Linspace e Arange
As funções do numpy linspace e arange tem o mesmo objetivo
Step30:  Já na função arange, define-se o intervalo semi-aberto [inicio,fim) e o passo que será dado entre um elemento e outro. 
Dessa forma, para gerar
um numpy.array entre 0 e 1 com 10 elementos, temos que calcular o passo (0.1) e passar esse passo como parâmetro.
Step31:  Confirme que a principal diferença entre os dois que pode ser verificada nos exemplos acima é que 
no linspace o limite superior da distribuição é inclusivo (intervalo fechado), 
enquanto no arange isso não ocorre (intervalo semi-aberto).
Funções indices e meshgrid
As funções indices e meshgrid são extremamente úteis na geração de imagens sintéticas e o seu aprendizado permite também
entender as vantagens de programação matricial, evitando-se a varredura seqüencial da imagem muito usual na programação na linguagem C.
Operador indices em pequenos exemplos numéricos
A função indices recebe como parâmetros uma tupla com as dimensões (H,W) das matrizes a serem criadas. No exemplo a seguir, estamos
gerando matrizes de 5 linhas e 10 colunas. Esta função retorna uma tupla de duas matrizes que podem ser obtidas fazendo suas atribuições
como no exemplo a seguir onde criamos as matrizes r e c, ambas de tamanho (5,10), isto é, 5 linhas e 10 colunas
Step32:  Note que a matriz r é uma matriz onde cada elemento é a sua coordenada linha e a matriz c é uma matriz onde cada elemento é
a sua coordenada coluna. Desta forma, qualquer operação matricial feita com r e c, na realidade você está processando as
coordenadas da matriz. Assim, é possível gerar diversas imagens sintéticas a partir de uma função de suas coordenadas.
Como o NumPy processa as matrizes diretamente, sem a necessidade de fazer um for explícito, a notação do programa fica bem simples
e a eficiência também. O único inconveniente é o uso da memória para se calcular as matrizes de índices r e c. Iremos
ver mais à frente que isto pode ser minimizado. 
Por exemplo seja a função que seja a soma de suas coordenadas $f(r,c) = r + c$
Step33:  Ou ainda a função diferença entre a coordenada linha e coluna $f(r,c) = r - c$
Step34:  Ou ainda a função $f(r,c) = (r + c) \% 2$ onde % é operador módulo. Esta função retorna 1 se a soma das coordenadas for ímpar e 0 caso contrário.
É uma imagem no estilo de um tabuleiro de xadrez de valores 0 e 1
Step35:  Ou ainda a função de uma reta $f(r,c) = (r = \frac{1}{2}c)$
Step36:  Ou ainda a função parabólica dada pela soma do quadrado de suas coordenadas $f(r,c) = r^2 + c^2$
Step37:  Ou ainda a função do círculo de raio 4, com centro em (0,0) $f(r,c) = (r^2 + c^2 < 4^2)$
Step38:  Operador indices em exemplo de imagens sintéticas
Vejamos os exemplos acima, porém gerados em imagens. A diferença será no tamanho da matriz, iremos utilizar matriz (200,300), e
a forma de visualizá-la através do adshow, ao invés de imprimir os valores como fizemos acima. Gerando as coordenadas utilizando indices
Step39:  Soma
Função soma
Step40:  Subtração
Função subtração $f(r,c) = r - c$
Step41:  Xadrez
Função xadrez $f(r,c) = \frac{(r + c)}{8} \% 2$. Aqui foi feita a divisão por 8 para que o tamanho das casas do xadrez fique 8 x 8, caso
contrário é muito difícil de visualizar o efeito xadrez pois a imagem possui muitos pixels.
Step42:  Reta
Ou ainda a função de uma reta $f(r,c) = (r = \frac{1}{2} c)$
Step43:  Parábola
Função parabólica
Step44:  Círculo
Função do círculo de raio 190, $f(r,c) = (r^2 + c^2 < 190^2)$
Step45:  Meshgrid
A função meshgrid é semelhante à  função indices visto
anteriormente, porém, enquanto indices gera as coordenadas inteiras não negativas a partir de um shape(H,W), 
o meshgrid gera os valores das matrizes a partir de dois vetores de valores reais quaisquer, um para as linhas e outro para as colunas.
Veja a seguir um pequeno exemplo numérico. Para que o meshgrid fique compatível com a nossa convenção de (linhas,colunas), deve-se
usar o parâmetro indexing='ij'.
Step46:  Gerando os vetores com linspace
A função linspace gera vetor em ponto flutuante recebendo os parâmetro de valor inicial, valor final e número de pontos do vetor.
Desta forma ele é bastante usado para gerar os parâmetro para o meshgrid.
Repetindo os mesmos valores do exemplo anterior, porém usando linspace. Observe que o primeiro vetor possui 5 pontos,
começando com valor -1.5 e o valor final é 0.5 (inclusive). O segundo vetor possui 6 pontos, começando de -20 até 30
Step47:  Usando os dois vetores gerados pelo linspace no meshgrid
Step48:  Podemos agora gerar uma matriz ou imagem que seja função destes valores. Por exemplo ser o produto deles
Step49:  Exemplo na geração da imagem sinc com meshgrid
Neste exemplo, geramos a imagem da função $sinc(r,c)$ em duas dimensões, nos intervalos na vertical, de -5 a 5 e na
horizontal de -6 a 6. A função sinc é uma função trigonométrica que pode ser utilizada para filtragens.
A equação é dada por
Step50:  Exemplo na geração da imagem sinc com indices
Outra forma de gerar a mesma imagem, usando a função indices é processar os
indices de modo a gerar os mesmos valores relativos à grade de espaçamento regular
acima, conforme ilustrado abaixo
Step51:  Verificando que as duas funções são iguais
Step52:  Para usuários avançados
Na realidade a função indices retorna um único array n-dimensional com uma dimensão a mais que o indicado pelo
shape usado como parâmetro. Assim, quando é feito r,c = np.indices((rows,cols)), r é atribuído para o elemento 0 e
c é atribuído para o elemento 1 do ndarray. No caso do meshgrid, ele retorna tantos arrays quanto forem o número
de vetores passados como parâmetro para meshgrid.
Tile
Uma função importante da biblioteca numpy é a tile, que gera repetições
do array passado com parâmetro. A quantidade de repetições é dada pelo 
parâmetro reps
Exemplo unidimensional - replicando as colunas
Step53:  Exemplo unidimensional - replicando as linhas
Para modificar as dimensões na quais a replicação será realizada
modifica-se o parâmetro reps, passando ao invés de um int, uma tupla
com as dimensões que se deseja alterar
Step54:  Exemplo bidimensional - replicando as colunas
Step55:  Exemplo bidimensional - replicando as linhas
Step56:  Exemplo bidimensional - replicando as linhas e colunas simultaneamente
Step57:  Resize
A função np.resize recebe um array  a e retorna um array com o shape desejado. Caso o novo array
seja maior que o array original o novo array é preenchido com cópias de a.
Step58:  Clip
A função clip substitui os valores de um array que estejam abaixo de um limiar mínimo ou que estejam acima de um limiar máximo,
por esses limiares mínimo e máximo, respectivamente. Esta função é especialmente útil em processamento de imagens para evitar 
que os índices ultrapassem os limites das imagens.
Exemplos
Step59:  Exemplo com ponto flutuante
Observe que se os parâmetros do clip estiverem em ponto flutuante, o resultado também será em ponto flutuante
Step60:  Formatando arrays para impressão
Imprimindo arrays de ponto flutuante
Ao se imprimir arrays com valores em ponto flutuante, o NumPy em geral, imprime o array com muitas as casas
decimais e com notação científica, o que dificulta a visualização.
Step61:  É possível diminuir o número de casas decimais e suprimir a notação exponencial utilizando
a função set_printoption do numpy
Step62:  Imprimindo arrays binários
Array booleanos são impressos com as palavras True e False, como no exemplo a seguir
Step63:  Para facilitar a visualização destes arrays, é possível converter os valores para inteiros utilizando
o método astype(int)
<ASSISTANT_TASK:>
Python Code:
import numpy as np
a = np.array( [2,3,4,-1,-2] )
print('Dimensões: a.shape=', a.shape )
print('Tipo dos elementos: a.dtype=', a.dtype )
print('Imprimindo o array completo:\n a=',a )
Explanation: Introdução ao NumPy
O tipo ndarray
O tipo ndarray, ou apenas array é um arranjo de itens homogêneos de dimensionalidade N, indexados por uma tupla de N inteiros. Existem 3 informações essenciais associadas ao ndarray: o tipo dos dados, suas dimensões e seus dados em si. A propriedade dtype permite saber o tipo de dados
enquanto que shape é uma tupla que indica o tamanho de cada dimensão do arranjo. O acesso aos dados em si deve ser feito por indexação, por fatiamento ou pela variável em si. 
Existem várias maneiras de criar uma variável do tipo ndarray.
Por exemplo, é possível criar uma a partir de uma lista (1D) ou lista de listas usando a função array. 
O tipo de matriz resultante é deduzida a partir do tipo de elementos nas sequências.
Veja a seguir um vetor de inteiros com 5 elementos. Note que o vetor é uma linha com 5 colunas. Observe também que o shape é uma tupla de um único elemento (veja a vírgula que aparece por ser uma tupla).
End of explanation
b = np.array( [ [1.5, 2.3, 5.2],
                [4.2, 5.6, 4.4] ] )
print('Um array bidimensional, dimensões:(b.shape=', b.shape )
print('Tipo dos elementos: b.dtype', b.dtype )
print('Número de colunas:', b.shape[-1] )
print('Número de linhas:', b.shape[-2] )
print('Elementos, b=\n', b )
Explanation: Veja a seguir uma matriz bidimensional de dados ponto flutuante de 2 linhas e 3 colunas. Observe que a tupla do shape aumenta para a esquerda,
isto é, se eu tiver um vetor de 3 elementos, o seu shape é (3,) e se uma nova dimensão for adicionada, por exemplo 2 linhas e 3 colunas, o
shape passa a ser (3,2). O que antes shape[0] no vetor unidimensional era colunas, já na matriz bidimensional shape[0] passou a ser o número
de linhas.
Assim o último elemento da tupla do shape indica o número de colunas, a penúltima o número de linhas. Assim se quisermos sempre buscar
o número de colunas, independentemente do número de dimensões, shape[-1] informa sempre o número de colunas, shape[-2], o número de linhas.
End of explanation
d = np.zeros( (2,4) )
print('Array de 0s: \n', d )
d = np.ones( (3,2,5), dtype='int16' )    
print('\n\nArray de 1s: \n', d )
d = np.empty( (2,3), 'bool' )
print('Array não inicializado (vazio):\n', d )
Explanation: Manipulação de arrays
Criando arrays inicializados
É possível criar arrays de zeros, uns ou valores não inicializados usando as funções zeros, ones ou empty. As dimensões
do array são obrigatórias e é dado por uma tupla e o tipo dos elementos é opcional, sendo que o default é tipo float. 
O código a seguir cria 3 arrays. O primeiro possui 2 linhas e 4 colunas. O segundo tem 3 dimensões: 3 fatias (ou imagens) onde cada
uma tem 2 linhas e 5 colunas. Por último, é criado uma matriz booleana (True e False) de valores não inicializados 
de 2 linhas e 3 colunas. A vantagem do empty é que ele é mais rápido que o zeros ou ones. No caso abaixo, os valores
mostrados na matrix criada pelo empty são aleatórios.
End of explanation
print('np.arange( 10) = ', np.arange(10) )
print('np.arange( 3, 8) = ', np.arange(3,8) )
print('np.arange( 0, 2, 0.5) = ', np.arange(0, 2, 0.5) )
print('np.linspace( 0, 2, 5 ) = ', np.linspace( 0, 2, 5 ) )
Explanation: Note que o Numpy permite arrays n-dimensionais. Em imagens em níveis de cinza iremos trabalhar com matrizes bidimensionais mas
se a imagem for colorida, iremos representá-la em 3 canais, R, G e B, representados na estrutura com 3 dimensões. 
Se for um vídeo, isto é, uma sequência de imagens, teremos que adicionar mais uma dimensão. 
Se for uma tomografia, também podemos representar em 3 dimensões: largura, altura e profundidade.
Criando arrays com valores sequenciais
As funções arange e linspace geram um vetor sequencial. Eles diferem apenas nos parâmetros. Enquanto o arange gera uma sequência a partir dos valores inicial (includente e opcional), final( excludente) e passo (opcional), linspace gera uma sequência com valores inicial e final e número de elementos. Note as diferenças nos exemplos a seguir:
End of explanation
a = np.arange(20) # a é um vetor de dimensão 20
print('a = \n', a )
Explanation: Veja que no último caso, usando o linspace, a sequência começa em 0 e termina em 2 e deve possuir 5 elementos. Veja que
para isto o passo a ser utilizado será 0.5, calculado automaticamente. Já no exemplo anterior, a sequência começa em 0 e deve terminar antes de 2 e o passo é 0.5. 
Fatiamento em narray unidimensional
Um recurso importante do numpy é o fatiamento no qual é possível acessar
um subconjunto do array de diversas formas. O fatiamento define os índices
onde o array será acessado definindo o ponto inicial, final e o passo entre
eles, nesta ordem: [inicial:final:passo].
Inicializando um array unidimensional
End of explanation
a = np.arange(20)
print('Resultado da operação a[1:15:2]' )
print(a[1:15:2]  )
Explanation: Exemplo simples de fatiamento
Para a realização do fatiamento são utilizados 3 parâmetros, colocados no local do índice do array.
Os 3 parâmetros são separados por dois pontos ":". Todos os 3 parâmetros podem ser opcionais que ocorrem
quando o valor inicial é 0, o valor final é o tamanho do array e o passo é 1. Lembrar que a ordem deles 
é: [inicial:final:passo]. Se o passo for 1 fica: [inicial:final]. Se o início for 0 fica: [:final] e se
o final for o último fica: [inicio:] e se forem todos [:].
O fatiamento é feito começando pelo primeiro valor, adicionando-se o passo até antes do último valor. Três
aspectos são extremamente importantes de serem lembrados: O índice inicial começa em zero, o índice final
nunca é atingido, o último índice utilizado é sempre o imediatamente anterior e o Numpy admite índices negativos, que
é uma indexação do último (-1) até o primeiro elemento (-W).
Os exemplos a seguir ajudam a fixar estes conceitos.
O código abaixo acessa os elementos ímpares começando de 1 até 14:
End of explanation
a = np.arange(20)
print('Resultado da operação a[1:-1:2]' )
print(a[1:-1:2] )
print('Note que o fatiamento termina antes do último elemento (-1)'  )
Explanation: Exemplo de fatiamento com indices negativos
Acessando o último elemento com índice negativo
O código abaixo acessa os elementos ímpares até antes do último elemento:
End of explanation
a = np.arange(20)
print('Resultado da operação a[-3:2:-1]' )
print(a[-3:2:-1] )
print( 'Note que o fatiamento retorna o array invertido' )
print( 'Antepenúltimo até o terceiro elemento com step = -1' )
Explanation: Inversão do array com step negativo (step = -1)
End of explanation
a = np.arange(20)
print('Resultado da operação a[:15:2]' )
print(a[:15:2] )
print('Note que o fatiamento inicia do primeiro elemento' )
print('Primeiro elemento até antes do 15o com passo duplo' )
Explanation: Fatiamento avançado
É possível realizar o fatiamento utilizando os 3 parâmetros explícitos
( o limite inferior, limite superior e o step), ou podemos suprimir algum 
desses parâmetros. Nestes casos a função toma o valor defaut: limite 
inferior = primeiro elemento, limite superior = último elemento e step = 1.
|Proposta inicial     | Equivalente |
|---------------------|-------------|
|a[0:len(a):1]        | a[:]        |
|a[0:10:1]            | a[:10]      |
|a[0:10:2]            | a[:10:2]    |
|a[2:len(a):1]        | a[2::]      |
|a[2:len(a):2]        | a[2::2]     |
Supressão do indice limite inferior
Quando o índice do limite inferior é omitido, é subentendido que é 0:
End of explanation
a = np.arange(20)
print('Resultado da operação a[1::2]' )
print(a[1::2] )
print('Note que o fatiamento termina último elemento' )
print('Primeiro elemento até o último com passo duplo' )
Explanation: Supressão do indice limite superior
Quando o índice do limite superior é omitido, fica implícito 
que é o último elemento:
End of explanation
a = np.arange(20)
print('Resultado da operação a[1:15]' )
print(a[1:15] )
print('Note que o fatiamento tem step unitário' )
print('Primeiro elemento até antes do 15o com passo um' )
Explanation: Supressão do indice do step
O índice do step é opcional e quando não é indicado, seu valor é 1:
End of explanation
a = np.arange(20)
print('Resultado da operação a[:]' )
print(a[:]  )
print('Todos os elementos com passo unitário' )
Explanation: Todos os elementos com passo unitário
End of explanation
a = np.arange(20) # a é um vetor unidimensional de 20 elementos
print('a = \n', a )
a = a.reshape(4,5) # a agora é um array 4x5 (4 linhas por 5 colunas)
print('a.reshape(4,5) = \n', a )
Explanation: Fatiamento no ndarray bidimensional
Um recurso importante do numpy é o fatiamento no qual é possível acessar
partes do array de diversas formas, como pode ser visto abaixo:
Inicializando um array e mudando o seu shape
End of explanation
print('A segunda linha do array: \n', a[1,:] )  # A segunda linha é o índice 1
print(' A primeira coluna do array: \n', a[:,0] ) # A primeira coluna corresponde ao índice 0
Explanation: Fatiamento de linhas e colunas de um array
O operador : indica que todos os elementos naquela dimensão devem ser acessados.
End of explanation
print('Acessando as linhas do array de 2 em 2 começando pelo índice 0: \n', a[0::2,:]  )
print(' Acessando as linhas e colunas do array de 2 em 2 começando \
pela linha 0 e coluna 1: \n', a[0::2,1::2] )
Explanation: Fatiamento de elementos específicos de um array
End of explanation
b = a[-1:-3:-1,:]
print('Acesso as duas últimas linhas do array em ordem reversa, \
b = a[-1:-3:-1,:] = \n',a[-1:-3:-1,:] )
print('Acesso elemento na última linha e coluna do array, a[-1,-1] =', a[-1,-1] )
c = a[::-1,:]
print('Invertendo a ordem das linhas do array: c = a[::-1,:] = \n', a[::-1,:] )
Explanation: Fatiamento com índices invertidos
End of explanation
a = np.arange(6) 
b = a
print("a =\n",a )
print("b =\n",b )
b.shape = (2,3)                             # mudança no shape de b,
print("\na shape =",a.shape )               # altera o shape de a
b[0,0] = -1                                 # mudança no conteúdo de b
print("a =\n",a )                           # altera o conteudo de a  
print("\nid de a = ",id(a) )                # id é um identificador único de objeto
print("id de b = ",id(b) )                  # a e b possuem o mesmo id
print('np.may_share_memory(a,b):',np.may_share_memory(a,b) )
Explanation: Copiando variáveis ndarray
O ndarray foi projetado para acesso otimizado a uma grande quantidade de dados. Neste sentido, os conceitos
descritos a seguir sobre as três formas de cópias entre variáveis ditas sem cópia, cópia rasa (shallow) e
cópia profunda (deep) são fundamentais para uma codificação eficiente. Podemos dizer que um ndarray possui
o cabeçalho que contém dados pelas informações sobre o tipo do elemento, a dimensionalidade (shape) e 
passo ou deslocamento para o próximo elemento (strides) e os dados raster em si. A tabela
a seguir mostra a situação do cabeçalho e dos dados nos três tipos de cópias.
|Tipo                 | Cabeçalho:Type,Shape,Strides|Dados raster      | Exemplo    |
|---------------------|-----------------------------|------------------|------------|
|Sem cópia, apenas ref| apontador original          |apontador original| a = b      |
|Cópia rasa           | novo                        |apontador original|b=a.reshape |
|                     |                             |                  |slicing, a.T|
|Cópia profunda       | novo                        |novo              |a = b.copy()|
Sem cópia explícita, apenas referência
No caso abaixo, usaremos o comando normal de igual como atribuição do array a para o array b.
Verifica-se que tanto o shape como os dados de b são os mesmos de a. Tudo se passa como b
fosse apenas um apontador para a. Qualquer modificação em b é refletida em a.
End of explanation
def cc(a):
  return a
b = cc(a)
print("id de a = ",id(a) )
print("id de b = ",id(b) )
print('np.may_share_memory(a,b):',np.may_share_memory(a,b) )
Explanation: Observe que mesmo no retorno de uma função, a cópia explícita pode não acontecer. Veja o exemplo a
seguir de uma função que apenas retorna a variável de entrada:
End of explanation
a = np.arange(30)
print("a =\n", a )
b = a.reshape( (5, 6))
print("b =\n", b )
b[:, 0] = -1
print("a =\n", a )
c = a.reshape( (2, 3, 5) )
print("c =\n", c )
print('c.base is a:',c.base is a )
print('np.may_share_memory(a,c):',np.may_share_memory(a,c) )
Explanation: Cópia rasa
A cópia rasa é muito útil e extensivamente utilizada. É usada quando se quer indexar o array original 
através da mudança de dimensionalidade ou do 
refatiamento, porém sem a necessidade de realizar uma cópia dos dados raster. Desta forma consegue-se
uma otimização no acesso ao array n-dimensional. Existem várias formas onde a cópia rasa acontece,
sendo as principais:
1) no caso do reshape onde o número de elementos do ndarray é o mesmo, porém sua dimensionalidade
é alterada; 2) no caso de fatiamento onde um subarray é indexado; 3) no caso de transposição do array;
4) no caso de linearização do raster através do ravel().
entre outros.
Reshape
O exemplo a seguir mostra inicialmente a criação de um vetor unidimensional sequencial sendo "visto" de
forma bidimensional ou tridimensional.
End of explanation
a = np.zeros( (5, 6))
print('%s %s %s %s %s' % (type(a), np.shape(a), a.dtype, a.min(), a.max()) )
b = a[::2,::2]
print('%s %s %s %s %s' % (type(b), np.shape(b), b.dtype, b.min(), b.max()) )
b[:,:] = 1.
print('b=\n', b )
print('a=\n', a )
print('b.base is a:',b.base is a )
print('np.may_share_memory(a,b):',np.may_share_memory(a,b) )
Explanation: Slice - Fatiamento
O exemplo a seguir mostra a cópia rasa no uso de fatiamento. No exemplo, todos os elementos de linhas
e colunas pares são modificados para 1. CUIDADO: quando é feita a atribuição de b = 1., é importante
que b seja referenciado como ndarray na forma b[:,:], caso contrário, se fizermos b = 1., uma nova 
variável é criada.
End of explanation
a = np.arange(25).reshape((5,5))
print('a=\n',a )
b = a[:,0]
print('b=',b )
b[:] = np.arange(5)
print('b=',b )
print('a=\n',a  )
Explanation: Este outro exemplo é uma forma atraente de processar uma coluna de uma matriz bidimensional,
porém é preciso CUIDADO, pois o uso de b deve ser com b[:] se for atribuído um novo valor para
ele, caso contrário, se fizermos b = arange(5), uma nova variável é criada.
End of explanation
a = np.arange(24).reshape((4,6))
print('a:\n',a )
print('a.T:\n',a.T )
print('np.may_share_memory(a,a.T):',np.may_share_memory(a,a.T) )
Explanation: Transposto
A operação matricial de transposição que troca linhas por colunas produz também um view 
da imagem, sem necessidade de cópia:
End of explanation
a = np.arange(24).reshape((4,6))
print('a:\n',a )
av = a.ravel()
print('av.shape:',av.shape )
print('av:\n',av )
print('np.may_share_memory(a,av):',np.may_share_memory(a,av) )
Explanation: Ravel
Aplicando-se o método ravel() a um ndarray, gera-se um view do raster
linearizado (i.e. uma única dimensão) do ndarray.
End of explanation
b = a.copy()
c = np.array(a, copy=True)
print("id de a = ",id(a) )
print("id de b = ",id(b) )
print("id de c = ",id(c) )
Explanation: Cópia profunda
Cria uma copia completa do array, do seu shape e conteúdo. A recomendação é utilizar a 
função copy() para realizar a copia profunda, entretanto é possível conseguir a
copia profunda pelo np.array.
End of explanation
a = np.arange(20).reshape(5,4)
b = 2 * np.ones((5,4))    
c = np.arange(12,0,-1).reshape(4,3)
print('a=\n', a )
print('b=\n', b )
print('c=\n', c )
Explanation: Operações matriciais
Uma das principais vantagens da estrutura ndarray é sua habilidade de processamento matricial.
Assim, para se multiplicar todos os elementos de um array por um escalar basta escrever a * 5 por
exemplo. Para se fazer qualquer operação lógica ou aritmética entre arrays, basta escrever a <oper> b:
End of explanation
b5 = 5 * b
print('b5=\n', b5 )
Explanation: Multiplicação de array por escalar: b x 5
End of explanation
amb = a + b
print('amb=\n', amb )
Explanation: Soma de arrays: a + b
End of explanation
at = a.T
print('a.shape=',a.shape )
print('a.T.shape=',a.T.shape )    
print('a=\n', a )
print('at=\n', at )
Explanation: Transposta de uma matriz: a.T
A transposta de uma matriz, troca os eixos das coordenadas. O elemento que
estava na posição (r,c) vai agora estar na posição (c,r). O shape
da matriz resultante ficará portanto com os valores trocados. A operação
de transposição é feita através de cópia rasa, portanto é uma operação
muito eficiente e deve ser utilizada sempre que possível.
Veja o exemplo a seguir:
End of explanation
ac = a.dot(c)
print('a.shape:',a.shape )
print('c.shape:',c.shape )
print('a=\n',a )
print('c=\n',c )
print('ac=\n', ac )
print('ac.shape:',ac.shape )
Explanation: Multiplicação de matrizes: a x c
A multiplicação de matrizes é feita através do operador dot. 
Para que a multiplicação seja possível é importante que o número de
colunas do primeiro ndarray seja igual ao número de linhas do
segundo. As dimensões do resultado será o número de linhas do 
primeiro ndarray pelo número de colunas do segundo ndarray. Confira:
End of explanation
# gera um numpy.array de 10 elementos, linearmente espaçados entre 0 a 1
print(np.linspace(0, 1.0, num=10).round(2) ) 
Explanation: Linspace e Arange
As funções do numpy linspace e arange tem o mesmo objetivo: gerar numpy.arrays linearmente
espaçados em um intervalo indicado como parâmetro.
A diferença primordial entre essas funções é como será realizada a divisão no intervalo especificado. 
Na função linspace essa divisão é feita através da definição do intervalo fechado [inicio,fim], isto é, contém o
início e o fim, e da quantidade de 
elementos que o numpy.array final terá. O passo portanto é calculado como (fim - inicio)/(n - 1). 
Dessa forma, se queremos gerar um numpy.array entre 0 e 1 com 10 elementos, utilizaremos o linspace da seguinte forma
End of explanation
# gera um numpy.array linearmente espaçados entre 0 a 1 com passo 0.1
print(np.arange(0, 1.0, 0.1) )
Explanation: Já na função arange, define-se o intervalo semi-aberto [inicio,fim) e o passo que será dado entre um elemento e outro. 
Dessa forma, para gerar
um numpy.array entre 0 e 1 com 10 elementos, temos que calcular o passo (0.1) e passar esse passo como parâmetro.
End of explanation
r,c = np.indices( (5, 10) )
print('r=\n', r )
print('c=\n', c )
Explanation: Confirme que a principal diferença entre os dois que pode ser verificada nos exemplos acima é que 
no linspace o limite superior da distribuição é inclusivo (intervalo fechado), 
enquanto no arange isso não ocorre (intervalo semi-aberto).
Funções indices e meshgrid
As funções indices e meshgrid são extremamente úteis na geração de imagens sintéticas e o seu aprendizado permite também
entender as vantagens de programação matricial, evitando-se a varredura seqüencial da imagem muito usual na programação na linguagem C.
Operador indices em pequenos exemplos numéricos
A função indices recebe como parâmetros uma tupla com as dimensões (H,W) das matrizes a serem criadas. No exemplo a seguir, estamos
gerando matrizes de 5 linhas e 10 colunas. Esta função retorna uma tupla de duas matrizes que podem ser obtidas fazendo suas atribuições
como no exemplo a seguir onde criamos as matrizes r e c, ambas de tamanho (5,10), isto é, 5 linhas e 10 colunas:
End of explanation
f = r + c
print('f=\n', f )
Explanation: Note que a matriz r é uma matriz onde cada elemento é a sua coordenada linha e a matriz c é uma matriz onde cada elemento é
a sua coordenada coluna. Desta forma, qualquer operação matricial feita com r e c, na realidade você está processando as
coordenadas da matriz. Assim, é possível gerar diversas imagens sintéticas a partir de uma função de suas coordenadas.
Como o NumPy processa as matrizes diretamente, sem a necessidade de fazer um for explícito, a notação do programa fica bem simples
e a eficiência também. O único inconveniente é o uso da memória para se calcular as matrizes de índices r e c. Iremos
ver mais à frente que isto pode ser minimizado. 
Por exemplo seja a função que seja a soma de suas coordenadas $f(r,c) = r + c$:
End of explanation
f = r - c
print('f=\n', f )
Explanation: Ou ainda a função diferença entre a coordenada linha e coluna $f(r,c) = r - c$:
End of explanation
f = (r + c) % 2
print('f=\n', f )
Explanation: Ou ainda a função $f(r,c) = (r + c) \% 2$ onde % é operador módulo. Esta função retorna 1 se a soma das coordenadas for ímpar e 0 caso contrário.
É uma imagem no estilo de um tabuleiro de xadrez de valores 0 e 1:
End of explanation
f = (r == c//2)
print('f=\n', f )
Explanation: Ou ainda a função de uma reta $f(r,c) = (r = \frac{1}{2}c)$:
End of explanation
f = r**2 + c**2
print('f=\n', f )
Explanation: Ou ainda a função parabólica dada pela soma do quadrado de suas coordenadas $f(r,c) = r^2 + c^2$:
End of explanation
f = ((r**2 + c**2) < 4**2)
print('f=\n', f * 1 )
Explanation: Ou ainda a função do círculo de raio 4, com centro em (0,0) $f(r,c) = (r^2 + c^2 < 4^2)$:
End of explanation
# Diretiva para mostrar gráficos inline no notebook
%matplotlib inline 
import matplotlib.pylab as plt
r,c = np.indices( (200, 300) )
plt.subplot(121)
plt.imshow(r,cmap = 'gray')
plt.title("linhas")
plt.axis('off')
plt.subplot(122)
plt.imshow(c,cmap = 'gray')
plt.axis('off')
plt.title("colunas");
Explanation: Operador indices em exemplo de imagens sintéticas
Vejamos os exemplos acima, porém gerados em imagens. A diferença será no tamanho da matriz, iremos utilizar matriz (200,300), e
a forma de visualizá-la através do adshow, ao invés de imprimir os valores como fizemos acima. Gerando as coordenadas utilizando indices:
Observe que o parâmetro de indices é uma tupla. Verifique o número de parêntesis utilizados:
End of explanation
f = r + c
plt.imshow(f,cmap = 'gray')
plt.title("r+c")
plt.axis("off")
Explanation: Soma
Função soma: $f(r,c) = r + c$:
End of explanation
f = r - c
plt.imshow(f,cmap = 'gray')
plt.title("r-c")
plt.axis("off")
Explanation: Subtração
Função subtração $f(r,c) = r - c$:
End of explanation
f = (r//8 + c//8) % 2
plt.imshow(f,cmap = 'gray')
plt.title("(r+c)%2")
plt.axis("off")
Explanation: Xadrez
Função xadrez $f(r,c) = \frac{(r + c)}{8} \% 2$. Aqui foi feita a divisão por 8 para que o tamanho das casas do xadrez fique 8 x 8, caso
contrário é muito difícil de visualizar o efeito xadrez pois a imagem possui muitos pixels.:
End of explanation
f = (r == c//2)
plt.imshow(f,cmap = 'gray')
plt.title('r == c//2')
plt.axis("off")
Explanation: Reta
Ou ainda a função de uma reta $f(r,c) = (r = \frac{1}{2} c)$:
End of explanation
f = r**2 + c**2
plt.imshow(f,cmap = 'gray')
plt.title('r**2 + c**2')
plt.axis("off")
Explanation: Parábola
Função parabólica: $f(r,c) = r^2 + c^2$:
End of explanation
f = (((r-100)**2 + (c-100)**2) < 19**2)
plt.imshow(f,cmap = 'gray')
plt.title('((r-100)**2 + (c-100)**2) < 19**2')
plt.axis("off")
Explanation: Círculo
Função do círculo de raio 190, $f(r,c) = (r^2 + c^2 < 190^2)$:
End of explanation
import numpy as np
r, c = np.meshgrid( np.array([-1.5, -1.0, -0.5, 0.0, 0.5]), 
                    np.array([-20, -10, 0, 10, 20, 30]), indexing='ij')
print('r=\n',r )
print('c=\n',c )
Explanation: Meshgrid
A função meshgrid é semelhante à  função indices visto
anteriormente, porém, enquanto indices gera as coordenadas inteiras não negativas a partir de um shape(H,W), 
o meshgrid gera os valores das matrizes a partir de dois vetores de valores reais quaisquer, um para as linhas e outro para as colunas.
Veja a seguir um pequeno exemplo numérico. Para que o meshgrid fique compatível com a nossa convenção de (linhas,colunas), deve-se
usar o parâmetro indexing='ij'.
End of explanation
rows = np.linspace(-1.5, 0.5, 5)
cols = np.linspace(-20, 30, 6)
print('rows:', rows )
print('cols:', cols )
Explanation: Gerando os vetores com linspace
A função linspace gera vetor em ponto flutuante recebendo os parâmetro de valor inicial, valor final e número de pontos do vetor.
Desta forma ele é bastante usado para gerar os parâmetro para o meshgrid.
Repetindo os mesmos valores do exemplo anterior, porém usando linspace. Observe que o primeiro vetor possui 5 pontos,
começando com valor -1.5 e o valor final é 0.5 (inclusive). O segundo vetor possui 6 pontos, começando de -20 até 30:
End of explanation
r, c = np.meshgrid(rows, cols, indexing='ij')
print('r = \n', r )
print('c = \n', c )
Explanation: Usando os dois vetores gerados pelo linspace no meshgrid:
End of explanation
f = r * c
print('f=\n', f )
Explanation: Podemos agora gerar uma matriz ou imagem que seja função destes valores. Por exemplo ser o produto deles:
End of explanation
e = np.spacing(1) # epsilon to avoid 0/0
rows = np.linspace(-5.0, 5.0, 150) # coordenadas das linhas
cols = np.linspace(-6.0, 6.0, 180) # coordenadas das colunas
r, c = np.meshgrid(rows, cols, indexing='ij') # Grid de coordenadas estilo numpy
z = np.sin(r**2 + c**2 + e) / (r**2 + c**2 + e) # epsilon is added to avoid 0/0
plt.imshow(z,cmap = 'gray')
plt.title('Função sinc: sen(r² + c²)/(r²+c²) em duas dimensões')
plt.axis("off")
Explanation: Exemplo na geração da imagem sinc com meshgrid
Neste exemplo, geramos a imagem da função $sinc(r,c)$ em duas dimensões, nos intervalos na vertical, de -5 a 5 e na
horizontal de -6 a 6. A função sinc é uma função trigonométrica que pode ser utilizada para filtragens.
A equação é dada por:
$$ sinc(r,c) = \frac{\sin(r^2 + c^2)}{r^2 + c^2}, \text{para\ } -5 \leq r \leq 5, -6 \leq c \leq 6
$$
Na origem, tanto r como c são zeros, resultando uma divisão por zero. Entretanto pela teoria dos limites, $\frac{sin(x)}{x}$ é
igual a 1 quando $x$ é igual a zero.
Uma forma de se obter isto em ponto flutuante é somar tanto no numerador como no denominador um epsilon, que é a
menor valor em ponto flutuante. Epsilon pode ser obtido pela função np.spacing.
End of explanation
n_rows = len(rows)
n_cols = len(cols)
r,c = np.indices((n_rows,n_cols))
r = -5. + 10.*r.astype(float)/(n_rows-1)
c = -6. + 12.*c.astype(float)/(n_cols-1)
zi = np.sin(r**2 + c**2 + e) / (r**2 + c**2 + e) # epsilon is addes to avoid 0/0
plt.imshow(zi,cmap = 'gray')
plt.title('Função sinc: sin(r² + c²)/(r²+c²) em duas dimensões')
plt.axis("off")
Explanation: Exemplo na geração da imagem sinc com indices
Outra forma de gerar a mesma imagem, usando a função indices é processar os
indices de modo a gerar os mesmos valores relativos à grade de espaçamento regular
acima, conforme ilustrado abaixo:
End of explanation
print('Máxima diferença entre z e zi?', abs(z - zi).max() )
Explanation: Verificando que as duas funções são iguais:
End of explanation
a = np.array([0, 1, 2])
print('a = \n', a )
print()
print('Resultado da operação np.tile(a,2): \n',np.tile(a,2) )
Explanation: Para usuários avançados
Na realidade a função indices retorna um único array n-dimensional com uma dimensão a mais que o indicado pelo
shape usado como parâmetro. Assim, quando é feito r,c = np.indices((rows,cols)), r é atribuído para o elemento 0 e
c é atribuído para o elemento 1 do ndarray. No caso do meshgrid, ele retorna tantos arrays quanto forem o número
de vetores passados como parâmetro para meshgrid.
Tile
Uma função importante da biblioteca numpy é a tile, que gera repetições
do array passado com parâmetro. A quantidade de repetições é dada pelo 
parâmetro reps
Exemplo unidimensional - replicando as colunas
End of explanation
a = np.array([0, 1, 2])
print('a = \n', a )
print()
print('Resultado da operação np.tile(a,(2,1)): \n',np.tile(a,(2,1)) )
Explanation: Exemplo unidimensional - replicando as linhas
Para modificar as dimensões na quais a replicação será realizada
modifica-se o parâmetro reps, passando ao invés de um int, uma tupla
com as dimensões que se deseja alterar
End of explanation
a = np.arange(4).reshape(2,2)
print('a = \n', a )
print()
print('Resultado da operação np.tile(a,2): \n',np.tile(a,2) )
Explanation: Exemplo bidimensional - replicando as colunas
End of explanation
a = np.arange(4).reshape(2,2)
print('a = \n', a )
print()
print('Resultado da operação np.tile(a,(3,1)): \n',np.tile(a,(3,1)) )
Explanation: Exemplo bidimensional - replicando as linhas
End of explanation
a = np.arange(4).reshape(2,2)
print('a = \n', a )
print()
print('Resultado da operação np.tile(a,(2,2)): \n',np.tile(a,(2,2)) )
Explanation: Exemplo bidimensional - replicando as linhas e colunas simultaneamente
End of explanation
a = np.array([[0,1],[2,3]])
print('a = \n', a )
print()
print('np.resize(a,(1,7)) = \n', np.resize(a,(1,7)) )
print()
print('np.resize(a,(2,5)) = \n', np.resize(a,(2,5)) )
Explanation: Resize
A função np.resize recebe um array  a e retorna um array com o shape desejado. Caso o novo array
seja maior que o array original o novo array é preenchido com cópias de a.
End of explanation
a = np.array([11,1,2,3,4,5,12,-3,-4,7,4])
print('a = ',a )
print('np.clip(a,0,10) = ', np.clip(a,0,10) )
Explanation: Clip
A função clip substitui os valores de um array que estejam abaixo de um limiar mínimo ou que estejam acima de um limiar máximo,
por esses limiares mínimo e máximo, respectivamente. Esta função é especialmente útil em processamento de imagens para evitar 
que os índices ultrapassem os limites das imagens.
Exemplos
End of explanation
a = np.arange(10).astype(np.int)
print('a=',a )
print('np.clip(a,2.5,7.5)=',np.clip(a,2.5,7.5) )
Explanation: Exemplo com ponto flutuante
Observe que se os parâmetros do clip estiverem em ponto flutuante, o resultado também será em ponto flutuante:
End of explanation
A = np.exp(np.linspace(0.1,10,32)).reshape(4,8)/3000.
print('A: \n', A )
Explanation: Formatando arrays para impressão
Imprimindo arrays de ponto flutuante
Ao se imprimir arrays com valores em ponto flutuante, o NumPy em geral, imprime o array com muitas as casas
decimais e com notação científica, o que dificulta a visualização.
End of explanation
np.set_printoptions(suppress=True, precision=3)
print('A: \n', A )
Explanation: É possível diminuir o número de casas decimais e suprimir a notação exponencial utilizando
a função set_printoption do numpy:
End of explanation
A = np.random.rand(5,10) > 0.5
print('A = \n', A )
Explanation: Imprimindo arrays binários
Array booleanos são impressos com as palavras True e False, como no exemplo a seguir:
End of explanation
print ('A = \n', A.astype(int))
Explanation: Para facilitar a visualização destes arrays, é possível converter os valores para inteiros utilizando
o método astype(int):
End of explanation
<END_TASK>