<jupyter_start><jupyter_text>MAT281 Aplicaciones de la Matemática en la Ingeniería Tarea 1 La siguiente tarea debe realizarse de manera individual. No es posible copiar y pegar código de otros estudiantes. Pueden consultar internet (stackoverflow, por ejemplo), pero deben indicar sus fuentes cuando corresponda.**Nombre**: *[COLOCAR NOMBRE ACA]***Rol**: *[COLOCAR ROL ACA]***Fecha de entrega**: Martes 16 Octubre 2018. En esta tarea, enviaremos mensajes secretos dentro de imágenes.![images/hackerman.png](images/hackerman.png) Este proceso se llama [Esteganografía](https://es.wikipedia.org/wiki/Esteganograf%C3%ADa), y consiste en enviar un mensaje oculto en algún medio de manera que no sea aparente que contiene dicho mensaje. Un método de analógico sería escribir una carta a alguien pero poner con [tinta invisible](https://es.wikihow.com/escribir-un-mensaje-con-tinta-invisible) el contenido real que queremos transmitir. En esta tarea exploraremos el cómo enviar un mensaje en una imágen, para lo cual tomaremos varios supuestos que permitirán simplificar las funciones a utizar. Estos supuestos son los siguientes:1. La imagen a codificar/decodificar estará en formato png y sus dimensiones serán múltiplos de 8.1. El texto no está cifrado dentro de la imagen, sino que se almacena de manera simple en cada uno de sus pixeles. La frase que resume esta tarea es la siguiente:***Información = Bits + Contexto***La manera que guardamos la información es siempre la misma independientemente del tipo de información. Como es bien sabido, un bit puede tener sólo 2 estados: uno o cero, en secuencias como "10101111 101110100 11111011 000100010 ..." que pueden representar números, textos, imágenes, sonidos y todo lo que llamamos "digital". Un byte es una secuencia particular de 8 bits, y que puede por tanto codificar $2^8 = 256$ secuencias distintas. La manera en que se codifican números enteros y reales, textos, imágenes, sonidos y videos puede resultar complejo puesto que siempre se busca comprimir la información para almacenarla.La tarea se divide en las siguientes etapas:1. Importar las librerías necesarias: para revisar que contamos con todo lo necesario antes de seguir avanzando.2. Transformación de strings a bits y de bits a strings.3. Transformación de imagen a bits y de bits a imagen.4. Ensamblaje de funciones para insertar un mensaje en una imagen, o recuperar un mensaje de una imagen.  1. Definición de las librerías [0 puntos]Esta tarea requiere instalar sólo unas pocas librerías: `pillow` que reemplaza la Python Image Library (PIL), numpy y os.<jupyter_code>from PIL import Image
import numpy as np
import os<jupyter_output><empty_output><jupyter_text>Para instalar todo lo anterior puedes:(1) Instalar el ambiente```conda env create -n mat281_tarea1_test python=3.6conda install pillow numpy jupyter notebook```(2) Crear el ambiente usando:```conda create -n mat281_tarea1 python=3.6source activate mat281_tarea1conda install pillow numpy jupyter notebook```  2. Codificando texto [40 puntos]La primera tarea por realizar es convertir texto común y corriente y obtener sus bits respectivos. Para ello existen librerías expecíficas que trabajan con códificaciones de texto y de manera ultra-rápida. Pero prescindiremos de ellas para trabajar sólo con código python para mayor diversión.  2.1 ¿Cómo poder obtener los bits de un string?Un texto se codifica a partir de la codificación de los carácteres que lo componen. Inicialmente, el código ASCII  (American Standard Code for Information Interchange) permitía únicamente la escritura en inglés debido a que no contenía acentos ni otros caracteres especiales. Sin embargo, tenían la ventaja de utilizar una pequeñísima cantidad de bits en su codificación. Para poder representar los lenguajes alrededor del mundo fue necesario crear convenciones especiales que extendieran el ASCII, dando origen a las codificaciones "unicode", "utf-8" y otras. En esta tarea y gracias a la inteligencia de python, podemos utilizar texto común en español con o sin acentos, y signos de exclamación sin mayores complejidades.El siguiente código, que debes ejecutar, imprimirá en pantalla los primeros 128 carácteres:<jupyter_code>print("".join([chr(my_int) for my_int in range(128)]))<jupyter_output><empty_output><jupyter_text>Los pasos a desarrollar serán las siguientes:1. Obtener los carácteres de un string. Esto se obtiene fácilmente recorriendo el string utilizando un ciclo for. No es necesario crear una función.2. Obtener el entero asociado a la representación ordinaria (ascii) de un carácter. Esto se logra utilizando la función `ord` que ya posee python. 3. Transformar un int a una secuencia de bits. Esto es un poco más complejo y necesitaremos desarrollar una función `int_to_bits`.<jupyter_code>def int_to_bits(my_int, n_wanted_bits=8):
    """
    Toma un entero my_int y lo convierte en la lista de bits (0 ó 1).
    Se regresa esa lista con el número indicado de bits.
    """
    my_bits_string_with_header = bin(my_int)
    n_current_bits = len(my_bits_string_with_header) - 2
    my_bits_string = "0"*(n_wanted_bits - n_current_bits) + my_bits_string_with_header[2:]
    my_bit_list = [int(bit) for bit in my_bits_string]
    return my_bit_list

print(int_to_bits(42))<jupyter_output><empty_output><jupyter_text>Pregunta 1 [10 puntos]:Explora la función `int_to_bits` y responde:**1 ¿Qué hace la función `bin`? ¿Que evalúa para el entero 0? ¿Y para el entero 42?** *R: >***2 ¿Qué efecto tiene cambiar el parámetro `n_wanted_bits`***R: >*  Pregunta 2 [10 puntos]:Completa la función `string_to_bits` indicada a continuación para que regrese una lista de enteros con valores 0 ó 1 correspondientes a la representación en bits de las letras que lo componen. Algunos ejemplos de valores a obtener:```pythonstring_to_bits("a") = [0, 1, 1, 0, 0, 0, 0, 1]string_to_bits("ñ") = [1, 1, 1, 1, 0, 0, 0, 1]string_to_bits("hola") = [0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1]```<jupyter_code>def string_to_bits(my_string):
    """
    Converts a string into a list of the bits for each of its characters.
    """
    my_bit_list = []
    for char in my_string:
        my_int = ord(char)
        # INICIO CODIGO
        # << RESPUESTA DEL ALUMNO >>
        # FIN CODIGO
    return my_bit_list
        
my_bit_list = string_to_bits("ñ")
print(my_bit_list)<jupyter_output><empty_output><jupyter_text>2.2 ¿Cómo podemos obtener un string a partir de los bits?Para poder leer un mensaje oculto en una imagen necesitaremos poder recorrer el camino inverso, esto es, producir un string a partir de sus bits. Para ello necesitaremos:1. Recuperar el entero a partir de la representación de bits. Para ello definiremos una función `bits_to_int`.2. Recuperar un carácter a partir de un entero. Para ello usaremos la función `chr` que ya existe en python.2. Obtener un string a partir de una representación en bits, juntando los elementos anteriores. Para ello definiremos una función `bits_to_string`.<jupyter_code>def bits_to_int(my_bits):
    """
    Very inefficient way. Can you think of a more elegant solution :-)
    """
    my_int = 0
    for i, bit in enumerate(my_bits[::-1]):
        my_int += 2**i * bit
    return my_int

print(bits_to_int([0,]))
print(bits_to_int([1,1,1]))
print(bits_to_int([0,1,0,1,0, 1, 0]))<jupyter_output><empty_output><jupyter_text>Pregunta 3 [10 puntos]:¿Porqué la función `bits_to_int` anteriormente definida es ineficiente? ¿Qué podríamos hacer para hacerla más eficiente? ¿En que orden se recorre la lista `my_bits`?*R: >*  Pregunta 4 [10 puntos]:Completa la función `bits_to_string` indicada a continuación a partir de una lista de enteros con valores 0 ó 1 regrese el string correspondiente. Algunos ejemplos de valores a obtener:```pythonbits_to_string([0, 1, 1, 0, 0, 0, 0, 1]) = "a"bits_to_string([1, 1, 1, 1, 0, 0, 0, 1]) = "ñ"```Obs: La conversión en bits puede variar según el sistema operativo y la versión instalada. Lo importante es que las letras básicas (anscii tradicional) funcionen bien y que la convención sea única para convertir en bits y de vuelta sin problema.<jupyter_code>def bits_to_string(my_bit_list, n_bits=8):
    """
    Using a list of bits creates the string representation of it.
    """
    my_string = ""
    n_ints = len(my_bit_list)//8
    for n in range(n_ints):
        bits = my_bit_list[n*8:(n+1)*8]
        # INICIO DE CODIGO
        # << RESPUESTA DEL ALUMNO >> # usar por ejemplo my_char = chr(my_int)
        # INICIO DE CODIGO
    return my_string

my_bit_list = [1, 1, 1, 1, 0, 0, 0, 1]
my_string = bits_to_string(my_bit_list)
print(my_string)<jupyter_output><empty_output><jupyter_text>2.3 Jugando con stringsAntes de avanzar, veamos que hemos obtenido jugando con la representación de diversos textos. Para ello, proveeremos una función auxiliar que indicará si la conversión es exitosa e imprimirá algunas estadísticas de interés. Este tipo de funciones, si bien no es estrictamente necesario para el objetivo de la tarea, nos permite saber si todo está correcto antes de seguir avanzando y debería ser parte de las buenas prácticas a adquirir: ***Verificar cada paso o etapa que construye un resultado más grande***<jupyter_code>def check_conversion(my_original_string):
    """
    Chequea la conversion realizada por el string, calcula estadísticas e imprime cosas interesantes.
    """
    my_bit_list = string_to_bits(my_original_string)
    my_converted_string = bits_to_string(my_bit_list)
    if my_original_string!= my_converted_string:
        print("\nAlgo no funcionó. Los strings no son iguales.")
        print("String original:")
        print(my_original_string)
        print("String reconvertido:")
        print(my_converted_string)
    else:
        print("\n¡Conversión correcta! Los strings son idénticos.")
        print("String: {}".format(my_converted_string))
        print("Bit code: {}".format("".join(str(i) for i in my_bit_list)))

check_conversion("Only english content")
check_conversion("¿Español? - sin problemas - ¡Increíble!")
check_conversion("En français, s’il vous plaît? Pas de problème!")
check_conversion("¿Japonés?: 最高のプログラマ")
check_conversion("¡¿Emojis?!: 😀")<jupyter_output><empty_output><jupyter_text>Los textos anteriores pueden o no tener problemas de conversión dependiendo de la versión de python y notebook. El hecho que se representen correctamente en pantalla corresponde a los esfuerzos realizados en python 3 y jupyter notebook para poseer una codificación universal. Sin que nos demos cuenta, está reconociendo cuando juntar y separar los bytes para representar adecuadamente los distintos caracteres.  3. Definición de funciones de apertura de imagen. [30 puntos]Por simplicidad, trabajaremos sólo con formato png que se almacena directamente en formato RGB.Recordemos que una imagen puede representarse (entre otros formas) mediante el formato RGB, en donde se tienen 3 canales separados para el color rojo (R - red), el colog verde (G - green) y el color azul (B - blue). ![rgb_1](images/rgb_1.png)Una imagen de $n_{width} \times n_{height}$ pixeles se representa por tanto mediante 3 matrices de $n_{width} \times n_{height}$ que contienen valores entre 0 y 255. El valor 0 corresponde a la ausencia completa del color, mientras que el color 255 indica la presencia completa del color.![rgb_2](images/rgb_2.jpg)Es posible almacenar la información de la siguiente manera. Supongamos que quisiéramos enviar la letra `x`. Como podemos conocer usando la función que ya hemos construido, la `x` se representa en ascii por el entero 120, es decir, por la secuencia de bits `[0, 1, 1, 1, 1, 0, 0, 0]`. Supongamos que tenemos la imagen de $1 \times 4$ pixeles, representada en RGB:```R = [0, 100, 181, 255]G = [1, 103, 193, 243]B = [3, 105, 177, 242]```Podemos usar los bits menos significativos de los enteros asociados a cada color para enviar los bits de la letra x.Así, dejaremos el número como par si deseamos enviar el bit 0, y como impar si deseamos enviar el bit 1. En el ejemplo anterior, usaremos sólo los canales R y G, y dejaremos el canal B con todos los bits en 0 para indicar que no entrega información adicional. Recordemos que deseamos enviar `[0, 1, 1, 1, 1, 0, 0, 0]`.```R = [0, 101, 181, 255]G = [1, 102, 192, 242]B = [2, 104, 176, 242]```La imagen generada es completamente idéntica a la imagen original para el ojo humano, y sin embargo, ¡hemos logrado enviar información contenida en ella!  3.1 Abrir una imagen y obtener las matrices RGBLa primera etapa es abrir la imagen y convertirla a un array RGB. Esta función se entrega de manera explícita a continuación. Para mayor facilidad, se separan los canales R, G y B.<jupyter_code>def from_image_to_array(my_picture):
    PIL_image = Image.open(my_picture)
    pixels = np.array(PIL_image)
    R, G, B = pixels[:,:,0], pixels[:,:,1], pixels[:,:,2]
    return R, G, B

my_picture_filepath = os.path.join("images", "hackerman.png")
R, G, B = from_image_to_array(my_picture_filepath)
print(R.shape) # heigh x width<jupyter_output><empty_output><jupyter_text>Pregunta 5 [10 puntos]Nuestra imagen de prueba mide $240 \times 220$ pixeles. ¿Cuántos bits pueden codificarse? ¿Cuantas letras pueden enviarse si cada letra requiere 1 byte = 8 bits? ¿Y si cada letra requiere 2 bytes? *R: >*  3.2 Reseteando los bitsAntes de codificar nueva información es conveniente eliminar todos los bits y dejar en cero todos los bits significativos.Cabe recordar que es posible realizar operaciones vectoriales de manera eficiente utilizando las funciones nativas de numpy: `np.mod(x,2)` nos entregará el bit menos significativo de `x`.<jupyter_code>x = np.array([[1,2,3],[4,5,6]])
print("x=\n", x)
print("np.mod(x,2)=\n", np.mod(x,2))
x_flat = x.flatten()
print("x_flat = x.flatten()=\n",x_flat)
x_reshaped = x_flat.reshape(x.shape)
print("x_flat.reshape([x.shape])=\n", x_reshaped)<jupyter_output><empty_output><jupyter_text>Pregunta 6 [10 puntos]Completa la función `reset_bits` de manera que se regresen nuevas matrices R, G y B con los valores de los bits signitificativos todos seteados a 0.Por ejempo: ```R = [0, 100, 181, 255]G = [1, 103, 193, 243]B = [3, 105, 177, 242]```deberia quedar```R = [0, 100, 180, 254]G = [0, 102, 192, 242]B = [2, 104, 176, 242]```<jupyter_code>def reset_least_significant_bit(R, G, B):
    """
    Resets the least significant bits in matrices R, G and B.
    """
    new_R = R.copy()
    new_G = G.copy()
    new_B = B.copy()
    # START CODING HERE
    # << RESPUESTA DEL ALUMNO >>
    # START CODING HERE    
    return new_R, new_G, new_B

R = np.array([0, 100, 181, 255])
G = np.array([1, 103, 193, 243])
B = np.array([3, 105, 177, 242])

print(reset_least_significant_bit(R, G, B))<jupyter_output><empty_output><jupyter_text>3.2 Guardar como una imagen a partir de las matrices RGBFinalmente necesitamos poder guardar una imagen a partir de matrices R, G y B. Resulta importante que no guardemos con ningún tipo de compresión, pues de otro modo la imagen podría simplificarse y se perdería la información en los bits que vamos a codificar.<jupyter_code>def save_image_from_array(R, G, B, image_filepath):
    """
    Creates an array with the proper dimensions and stores 
    the corresponding image
    """
    height, width = R.shape
    my_pixel_array = np.zeros([height, width, 3], dtype="uint8") #must be uint8 for compatibility
    my_pixel_array[:,:,0] = R                               
    my_pixel_array[:,:,1] = G                               
    my_pixel_array[:,:,2] = B
    new_PIL_image = Image.fromarray(my_pixel_array, mode='RGB')
    new_PIL_image.save(image_filepath, compress_level=0)
    return<jupyter_output><empty_output><jupyter_text>3.3 Prueba de imagenPara verificar que todo esté funcionando correctamente, necesitamos una función que abra una imagen y luego guarde las matrices generadas. Problema 7 [10 puntos]Escriba la función `open_reset_and_save` que tome una imagen y la guarde en otra imagen después de haber reseato sus dígitos menos significativos. Verifica que la imagen generada se vea parecida a la original.<jupyter_code>def open_reset_and_save(original_image, new_image):
    """
    Abre la imagen, genera las matrices y la guarda como imagen.
    """
    # Obtener las matrices R, G y B
    R, G, B = from_image_to_array(original_image)
    # Resetear las matrices
    # INICIO CODIGO
    # << RESPUESTA DEL ALUMNO >>
    # FIN CODIGO
    # Save the image                           
    save_image_from_array(R, G, B, new_image)
    print("Image saved at {}".format(new_image))
    return

image_1 = os.path.join("images", "hackerman.png")
image_2 = os.path.join("images", "hackerman_test.png")
open_reset_and_save(image_1, image_2)<jupyter_output><empty_output><jupyter_text>4. Juntando todo [30 puntos]A partir de las funciones anteriormente definidas, es posible1. Insertar un mensaje en una imagen.2. Leer un mensaje de una imagen.  4.1 Insertando un mensajeLos pasos necesarios son los siguientes:1. Tomar un string y convertir todas sus letras a una secuencia de bits.2. Tomar los bits y ponerlos en el dígito menos significativo de los enteros que codifican el color de una imagen.3. Guardar la imagenResulta necesario establecer ciertas convenciones para que la lectura y la escritura se realicen correctamente.Podríamos sobre-escribir los bits de sólo un canal, para ser más reservados. O escribir los bits en orden alterno en las matrices. O en general, aplicar cualquier cifrado a nuestro texto y al lugar donde enviaremos información. Sin embargo, no nos complicaremos. En este caso, escribiremos los bits en los enteros de la matriz R en el orden natural (por filas). Si todavía quedan bits, procederemos de la misma manera en la matriz G. Y por último, y si todavía queda texto, en la matriz B. Para poder juntar todos los ingredientes necesitamos una función auxiliar que copie los bits que se le indican en la matriz correspondiente.<jupyter_code>def copy_bits(X, bits, start, n_bits_to_copy):
    """
    Copia b_bits_to_copy de la lista bits en la matriz X partiendo en start.
    """
    # Compute auxiliar indexes
    end = start + n_bits_to_copy
    # Compute matrix size
    nx, ny = X.shape
    # Flatten the array to simplify copying values
    X_flat = X.flatten()
    # Copy the bits
    X_flat[:n_bits_to_copy] = X_flat[:n_bits_to_copy] + bits[start:end]
    # Reshape
    X_with_bits = X_flat.reshape([nx, ny])
    return X_with_bits<jupyter_output><empty_output><jupyter_text>Finalmente podemos ensamblar todo de manera apropiada:<jupyter_code>def encode(original_image, new_image, hidden_message):
    """
    Codificar el texto hidden_message 
    sobre la image original_image 
    para generar la imagen new_image
    """
    # Obtener los bits del mensaje secreto
    my_bit_list = string_to_bits(hidden_message)
    #print("".join([str(x) for x in my_bit_list]))
    # Calcular el número de bits por escribir
    n_bits = len(my_bit_list)
    # Obtener las matrices R, G y B
    R, G, B = from_image_to_array(original_image)
    # How many pixels do we have
    n_pixels = R.shape[0] * R.shape[1]
    # Resetear las matrices
    R, G, B = reset_least_significant_bit(R, G, B)
    # Know how much to copy
    n_layers = n_bits // n_pixels
    # Create the start and end for each layer
    R_start_index = 0
    G_start_index = n_pixels
    B_start_index = 2*n_pixels
    # Create the number of bits to copy at each layer
    if n_layers==0:
        print("Hidding message only in layer R")
        R_bits_to_copy = min(n_bits, n_pixels)
        G_bits_to_copy = 0
        B_bits_to_copy = 0
    elif n_layers==1:
        print("Hidding message in layers R and G")
        R_bits_to_copy = n_pixels
        G_bits_to_copy = min(n_bits, 2*n_pixels) - n_pixels
        B_bits_to_copy = 0
    else:
        print("Hidding message in layers R, G and B")
        R_bits_to_copy = n_pixels
        G_bits_to_copy = n_pixels
        B_bits_to_copy = min(n_bits, 3*n_pixels) - 2*n_pixels
    # Store the information
    R = copy_bits(R, my_bit_list, R_start_index, R_bits_to_copy)
    G = copy_bits(G, my_bit_list, G_start_index, G_bits_to_copy)
    B = copy_bits(B, my_bit_list, B_start_index, B_bits_to_copy)
    # Save the image                           
    save_image_from_array(R, G, B, new_image)
    print("Image saved at {}".format(new_image))
    return
                               
my_secret_string = "Aca puedo poner mensajes secretos y ocultarlos en una imagen."

image_1 = os.path.join("images", "hackerman.png")
image_2 = os.path.join("images", "hackerman_with_hidden_message.png")
encode(image_1, image_2, my_secret_string)<jupyter_output><empty_output><jupyter_text>Problema 8 [10 puntos]Responda a las siguientes preguntas:* ¿Porqué creamos una función auxiliar `copy_bits`?*R: >** ¿Que otra estrategia propondría para aumentar la seguridad en el envío del mensaje?*R: >*  4.2 Leyendo un mensajeLos pasos necesarios son los siguientes:1. Abrir una imagen y obtener los colores que componen una imagen.2. Tomar los bits menos significativos de los enteros asociados.3. Generar el texto asociado a esos bits e imprimirlos en pantalla.Esta etapa es más sencilla y solo requiere agrupar adecuadamente la información.<jupyter_code>def decode(image_with_message):
    """
    Abrir la imagen, separar en R, G, B
    Obtener los bits menos significativos de cada canal
    Formar el texto corresondiente.
    """
    # Get the ingredients
    R, G, B = from_image_to_array(image_with_message)
    # Use some strategy to store the information
    my_R_bits = list( np.mod(R.flatten(), 2) )
    my_B_bits = list(G.flatten() - 2*np.floor_divide(G.flatten(), 2))
    my_G_bits = list(B.flatten() % 2)
    my_bit_list = my_R_bits + my_B_bits + my_G_bits
    #print("".join([str(i) for i in my_bit_list]))
    # See if a message
    message = bits_to_string(my_bit_list, n_bits=8)
    return message
                               
image_2 = os.path.join("images", "hackerman_with_hidden_message.png")
string = decode(image_2)
print(string)<jupyter_output><empty_output><jupyter_text>Problema 9 [10 puntos]Explique el significado de todas las siguientes líneas en el código:```pythonmy_R_bits = list( np.mod(R.flatten(), 2) )my_B_bits = list(G.flatten() - 2*np.floor_divide(G.flatten(), 2))my_G_bits = list(B.flatten() % 2)my_bit_list = my_R_bits + my_B_bits + my_G_bits```*R: >*  Problema 10 [10 puntos]¿Que texto está codificado en la imagen `problema_10.png`?![utfsm](images/problema_10.png)<jupyter_code># INICIO DEL CODIGO

# CODIGO ACA

# FIN DEL CODIGO<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08300552486187845, 'fraction_numerical': 0.022939226519337018, 'mean_word_length': 4.336320754716981, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 80, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '332832', 'license': 'MIT', 'n_tokens_mistral': 8167, 'n_tokens_neox': 7764, 'n_words': 2764, 'path': 'tarea/tarea.ipynb', 'repo_name': 'sebastiandres/mat821_introduction', 'size': '22625'}