Skip to content

Sesion Laboratorio 11 Practica 4 1

Jesús Parrado Alameda edited this page Apr 18, 2023 · 1 revision

Sesión Laboratorio 11: Práctica 5-1

  • Tiempo: 2h
  • Fecha: Jueves, 20 de Abril de 2023
  • Objetivos de la sesión:
    • Presentar la práctica 4
    • Aprender a acceder a los píxeles de las imágenes
    • Hacer nuestro primer filtro
    • Usar los deslizadores para introducir umbrales

Contenido

Introducción

En esta práctica aprenderemos los fundamentos para manipular imágenes en las aplicaciones WEB.

Implementaremos filtros muy sencillos, como ejemplo de cómo se implementan las manipulaciones.

Se da por supuesto que el estudiante ya conoce la teoría sobre los filtros digitales de imágenes, y no es el objetivo de esta asignatura explicarlos, pero sí enseñar cómo implementarlos.

Ya sabemos cómo hacer dibujos en el canvas.

Ahora practicaremos leyendo el canvas como una imagen y aplicando filtros.

Esta es la práctica que realizó Ana Poveda García en el curso 2019-2020.

La puedes probar desde este enlace

Enunciado de la Práctica 4 (ESPECIFICACIONES)

Diseñar una aplicación en javascript en el navegador que muestra una imagen, la original.

A esta imagen se le aplicará un filtro, que se seleccionará entre dos filtros disponibles y se generará una segunda imagen con el resultado.

Los filtros se seleccionarán mediante dos botones.

Uno transformará la imagen a escala de grises.

El otro es un filtro de color, por umbrales.

Dispondrá de 3 barras deslizantes para seleccionar los umbrales de cada color: rojo, verde, azul.

Un umbral de 0 hace que ese color en concreto desaparezca.

Si el filtro seleccionado actualmente es el de grises, los deslizadores no tendrán ningún efecto.

Sólo funcionarán cuando esté activado el filtro de color.

Tanto el filtro de grises como el de color se aplican a la imagen original (y no a la imagen resultado de otro filtro).

Deberás incluir en el fichero README.md de la práctica 4 un manual de usuario básico explicando el funcionamiento de tu aplicación.

Deberás indicar explícitamente las mejoras que has introducido.

La aplicación deberá estar accesible desde la URL: https://TU-USUARIO-GITHUB.github.io/2020-2021-CSAAI-Practicas/P4/.

Mejoras

Una vez que cumplas las especificaciones, se deja a tu creatividad la inclusión de mejoras adicionales en la práctica.

Da rienda suelta a tu imaginación e implementa las que quieras.

Estas son algunas propuestas:

  • Aplicación de los filtros a diferentes imágenes. Tener en una parte dos o más imágenes iniciales. El filtro se aplicará a la imagen que esté seleccionada (y sólo a ella)
  • Filtro adicional: Imagen especular. Dar la vuelta a la imagen, horizontalmente.
  • Filtro adicional: Imagen boca abajo.
  • Filtro adicional: Añadir ruido. Sumar o restar valores pequeños generados aleatoriamente para cada uno de los píxeles. De esta forma, cada pixel varía aleatoriamente (y ligeramente) sus valores.

Manipulación de imágenes y seguridad

Para manipular imágenes desde nuestros programas javascript utilizaremos el Canvas.

Mediante la función getImageData() obtendremos los datos en crudo de la imagen y así podremos leer y modificar la imagen.

Este método, sin embargo, puede ser usado para provocar ataques XSS.

Por ello, este método está protegido y genera excepciones de seguridad cuando se usa con imágenes no autorizadas.

Este es el típico mensaje de ERROR que se obtiene en la consola del navegador:

A efectos prácticos, esto significa que No podemos ejecutar el código javascritpt que use getImageData() directamente desde el navegador (bien directamente desde el Atom, con el previsualizador, o abriendo directamente el html con el protocolo file://), como hemos hecho hasta ahora, sino que las imágenes a usar sólo pueden estar en el mismo servidor en el que se encuentra la web.

La solución para trabajar en local es lanzar nuestro propio servidor web desde la carpeta donde tenemos nuestros ficheros html y js de nuestra práctica.

Esto lo podemos hacer fácilmente usando el servidor que viene integrado con en uno de los módulos de python.

Así, hay que abrir un terminal y ejecutar:

python3 -m http.server

(o llamando python en vez de python3, según la versión que tengas instalada)

Esto es válido para cualquier sistema operativo (Linux, Max, Windows), pero es necesario tener python instalado.

Al ejecutar este comando se lanza el servidor.

Para ver en el navegador nuestra práctica hay que acceder a esta URL: http://0.0.0.0:8000/

Puerto ocupado por otro servidor

Si el puerto 8000 está en uso verás un error en la consola, prueba con el siguiente:

python3 -m http.server 8001

Y accede al servidor con la URL: http://0.0.0.0:8001/

Nota sobre Windows

Si lo estás probando en una máquina windows, usa este comando para lanzar el servidor:

python -m http.server --bind 127.0.0.1 8000

y la URL será esta: http://127.0.0.1:8000

Ejemplo 1: Copiando una imagen al canvas

Manipularemos la imagen que esté situada sobre el canvas.

Por ello, el primer paso es colocar nuestra imagen origen en el canvas.

Esto lo hacemos invocando al método drawImage del contexto 2D del canvas.

Utilizaremos como imagen el logo de la URJC, pero puedes usar la que tú quieras: logoURJC.png

Es importante que te la descargues y la coloques en el mismo directorio que los ficheros html, css y js de tu práctica

Este es el código HTML usado en este ejemplo

  • Fichero HTML: Ej-01.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css">
    <script src="Ej-01.js" defer></script>
    <title>Ejemplo 1</title>
</head>
<body>
    <p>Imagen original</p>
    <img src="logoURJC.png" id="imagesrc">
    <p>Imagen manipulada</p>
    <canvas id="canvas"></canvas>
</body>
</html>

En la hoja de estilo simplemente se cambia el fondo a gris para que las imágenes, que tienen fondo blanco, resalten más.

  • Fichero CSS: style.css
body {
  background-color: grey;
}

El código javascript establece el tamaño del canvas para que sea igual al de la imagen original e inserta la imagen en él.

  • Fichero: Ej-01.js
console.log("Ejecutando JS....")

//-- Obtener elementos del DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('imagesrc')
const ctx = canvas.getContext('2d');

//-- Función de retrollamada de imagen cargada
//-- La imagen no se carga al instante, sino que
//-- lleva un tiempo. Sólo podemos acceder a ella una vez
//-- que esté totalmente cargada
img.onload = function () {

  console.log("Imagen cargada");

  //-- Se establece como tamaño del canvas el mismo
  //-- que el de la imagen original
  canvas.width = img.width;
  canvas.height = img.height;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavia
  ctx.drawImage(img, 0,0);
};

console.log("Fin...");

Ahora, para probarlo, lanzamos primero el servidor para evitar los problemas de seguridad (No hace falta lanzarlo cada vez, se ejecuta una vez y permanecerá ahí hasta que salgamos pulsando Ctrl-C).

Desde el navegador nos conectamos esta URL: http://0.0.0.0:8000/

(También valdrían estas: http://localhost:8000, http://127.0.0.1).

Abrimos la consola del desarrollador para ver lo que está ocurriendo.

En la parte superiores está la imagen original.

Y en la parte inferior está la imagen copiada en el canvas.

Esta será la que manipularemos.

También nos fijaremos en el orden en el que aparecen las trazas en la consola.

Vemos que el mensaje "Fin" que se ha situado en el código en la parte final, NO es lo último que aparece.

Lo último es el mensaje que hemos situado en la función de retrollamada de la carga de la imagen.

Esto es debido a que la imagen tarda un tiempo en cargarse.

Sólo cuando se ha terminado de cargar, se ejecuta su función de retrollamada (callback), que es justo lo que queríamos.

¡Ya lo tenemos todo listo para empezar a manipularla!

Ejemplo 2: Accediendo a los píxeles de la imagen

Las imágenes están formadas por unos puntitos muy pequeños, llamados píxeles.

Cada píxel está formado a su vez por tres componentes, una roja, una azul y una verde.

La mezcla de esos colores primarios genera el resto de colores.

Cada componente se indica mediante un byte, por lo que tiene valores entre 0 y 255.

Además se añade una cuarta componente, el grado de transparencia, también de un byte.

Por ello, cada píxel de la imagen se representa 4 bytes: 3 para las componentes roja, verde y azul y uno adicional para el grado de transparencia.

Para acceder a la información de los píxeles usamos el método getImageData() del contexto del canvas.

Se le pasan como parámetros la esquina superior izquierda y la inferior derecha de la región que queremos leer.

Para leer la imagen completa del canvas usaremos esta instrucción.

let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

El objeto creado, imgData, es el que contiene el array data con la información de todos los píxeles.

Para acceder más fácilmente lo situamos en una variable nueva, que llamaremos también data:

let data = imgData.data

Las primeras cuatro posiciones del array (0,1,2 y 3) se corresponden con el pixel (0,0), situado en la esquina superior izquierda, las siguientes cuatro con el situado a su derecha, en (1,0) y así sucesivamente hasta llegar al último elemento, que es el pixel de la esquina inferior derecha.

En esta figura se muestran las posiciones del array correspondientes a la imagen de prueba, que tiene 400x179 píxeles.

Puesto que cada pixel tiene 4 bytes, en las posiciones múltiplo de 4 se encuentran los canales rojos (data[i4)), en las siguientes (data[i4 + 1]) los verdes, luego los azules (data[i4 + 2) y finalmente la transparencia (data[i*4 +3).

En este código de ejemplo se accede al array data de la imagen y se muestran en la consola su tamaño.

Además, se muestran la altura y la anchura de la imagen, el total de píxeles y se calcula el tamaño que debería tener data para comprobar, que es correcto.

Así, el número total de píxeles se obtiene multiplicando la altura por la anchura.

Como cada pixel ocupa 4 bytes, en total el tamaño del array de datos debería ser de 4 por el número total de píxeles.

  • Fichero: Ej-02.js
console.log("Ejecutando JS....")

//-- Obtener elementos del DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('imagesrc')
const ctx = canvas.getContext('2d');

//-- Función de retrollamada de imagen cargada
//-- La imagen no se carga al instante, sino que
//-- lleva un tiempo. Sólo podemos acceder a ella una vez
//-- que esté totalmente cargada
img.onload = function () {

  //-- Se establece como tamaño del canvas el mismo
  //-- que el de la imagen original
  canvas.width = img.width;
  canvas.height = img.height;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavia
  ctx.drawImage(img, 0,0);

  //-- Obtener la imagen del canvas en pixeles
  let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  //-- Obtener el array con todos los píxeles
  let data = imgData.data

  //-- Obtener el numero total de elementos en el array
  console.log("Tamaño de data: " + data.length)

  //-- El número total de pixeles es la altura por la anchura
  npixels = canvas.width * canvas.height
  console.log("Anchura (en pixeles): " + canvas.width)
  console.log("Altura (en pixeles): " + canvas.height)
  console.log("Pixeles totales: " + npixels)

  //-- Puesto que cada pixel ocupa 4 bytes, el array de píxeles
  //-- tiene un tamaño de 4 * numero de pixeles
  console.log("Total de datos de la imagen: " + npixels * 4)
};

console.log("Fin...");

Al ejecutarlo en el navegador obtenemos esta información en la consola:

Efectivamente, el tamaño del array data coincide con los cálculos hechos: 286400 posiciones

Ejemplo 3: Pintando un pixel rojo

Llega el momento de manipular la imagen: cambiaremos un pixel al color rojo.

Para ello tenemos que elegir un pixel de una posición, por ejemplo el (200, 50) y cambiar sus canales.

Pondremos el canal rojo a tope (255) y los canales verde y azul a 0.

El de la transparencia lo dejaremos como esté.

Primero calculamos la posición del pixel en el array.

Es el pixel de la posición (200, 50): Hay 50 líneas de una anchura igual a canvas.width.

En total 50*canvas.width píxeles.

A esto le sumamos los 200 pixeles de la posición horizontal:

let i = 200 + 50*canvas.width;

Y ahora cambiamos los valores del pixel que está a partir de la posición i del array:

data[i*4] = 255;   //-- Canal rojo
data[i*4 + 1] = 0; //-- Canal verde
data[i*4 + 2] = 0; //-- Canal azul

Por último actualizamos la imagen del canvas con los nuevos datos.

Esto lo hacemos ejecutando el método putImageData()

ctx.putImageData(imgData, 0, 0);

El código completo es el siguiente:

  • Fichero: Ej-03.js
console.log("Ejecutando JS....")

//-- Obtener elementos del DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('imagesrc')
const ctx = canvas.getContext('2d');

//-- Función de retrollamada de imagen cargada
//-- La imagen no se carga instantaneamente, sino que
//-- lleva un tiempo. Sólo podemos acceder a ella una vez
//-- que esté totalmente cargada
img.onload = function () {

  //-- Se establece como tamaño del canvas el mismo
  //-- que el de la imagen original
  canvas.width = img.width;
  canvas.height = img.height;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavía
  ctx.drawImage(img, 0,0);

  //-- Obtener la imagen del canvas en pixeles
  let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  //-- Obtener el array con todos los píxeles
  let data = imgData.data

  //-- data[0] es el canal rojo del pixel de la posición 0,0
  //-- data[1] es el canal verde del pixel 0,0
  //-- data[2] es el canal azul del pixel 0,0
  //-- data[3] es el canal de transparencia del pixel 0,0
  //-- data[4] es el canal ROJO del pixel 1,0
  //-- ....
  //-- En general, para el pixel i
  //-- data[4*i] es el canal rojo
  //-- data[4*i + 1]: Canal verde
  //-- data[4*i + 2]: Canal azul
  //-- data[4*i + 3]: Canal de transparencia

  //-- Colocar un pixel rojo en (200,50)

  //-- Obtener el numero de pixel a partir de su posición
  let i = 200 + 50*canvas.width;

  //-- Pixel rojo: canal rojo a tope. Resto de colores a 0
  //-- La transparencia no se modifica
  data[i*4] = 255;    //-- Canal Rojo
  data[i*4 + 1] = 0;  //-- Canal Verde
  data[i*4 + 2] = 0;  //-- Canal azul

  //-- Poner la imagen modificada en el canvas
  ctx.putImageData(imgData, 0, 0);
};

console.log("Fin...");

Al ejecutarlo en el navegador veremos un pixel rojo en la zona blanca.

Es un pixel muy pequeño, hay que fijarse bien para verlo:

¡Nuestra primera manipulación de imagen!

Ejemplo 4: Eliminar el canal rojo de toda la imagen

Haremos nuestro primer filtro.

Uno que elimine toda la componente roja de la imagen original.

Para ello sólo hay que recorrer todos los píxeles del array poniendo a cero el rojo y dejando el resto igual.

Sabemos que esta componente está en las posiciones 0, 4, 8... i * 4 así que sólo hay que usar un bucle for que recorra estos valores y lo tendremos listo.

for (let i = 0; i < data.length; i+=4) {
    data[i] = 0; //-- Canal rojo a 0
  }

El código completo es este:

  • Fichero: Ej-04.js
console.log("Ejecutando JS....")

//-- Obtener elementos del DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('imagesrc')
const ctx = canvas.getContext('2d');

//-- Función de retrollamada de imagen cargada
//-- La imagen no se carga al instante, sino que
//-- lleva un tiempo. Sólo podemos acceder a ella una vez
//-- que esté totalmente cargada
img.onload = function () {

  //-- Se establece como tamaño del canvas el mismo
  //-- que el de la imagen original
  canvas.width = img.width;
  canvas.height = img.height;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavía
  ctx.drawImage(img, 0,0);

  //-- Obtener la imagen del canvas en pixeles
  let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  //-- Obtener el array con todos los píxeles
  let data = imgData.data

  //-- Eliminar el canal Rojo:recorrer el array de datos
  //-- eliminado el canal rojo y dejando el resto igual que
  //-- estaba
  for (let i = 0; i < data.length; i+=4) {
    data[i] = 0; //-- Canal rojo a 0
  }

  //-- Poner la imagen modificada en el canvas
  ctx.putImageData(imgData, 0, 0);

  console.log("hola....");
};

console.log("Fin...");

Al ejecutarlo en el navegador obtenemos esto:

Observamos que la corona encima de la U se ha transformado en negro.

El fondo, que era blanco, ahora es azul claro: sólo tiene componentes verde y azul.

Lo que era negro lo sigue siendo (el negro no tiene componente roja).

Ejemplo 5: Umbral de color rojo

Haremos un filtro usando el deslizador para variar el umbral de color rojo.

Los deslizadores los vimos en la Sesión 7 de Teoría

Todos aquellos píxeles cuyo nivel de rojo esté por encima del umbral, se limitarán al valor de este umbral.

O dicho de otra manera, el umbral determina el nivel máximo de rojo permitido.

Este es el nuevo fichero HTML.

Estamos utilizando esta imagen y debajo de ella se sitúa el deslizador.

La imagen original está oculta.

Sólo se muestra la final.

  • Fichero: Ej-05.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="Ej-05.css">
    <script src="Ej-05.js" defer></script>
    <title>Ejemplo 5</title>
</head>
<body>
    <canvas id="canvas"></canvas>
    <img src="FPGA.jpg" id="imagesrc">
    <p>Establece el umbral de rojo:</p>
    <input type="range" id="deslizador" min="0" max="255" value="255"/>
    <p id="range_value">00</p>
</body>
</html>

En el fichero de estilo indicamos que la imagen identificada como imagesrc no esté visible:

  • Fichero: Ej-05.css
#imagesrc {
  display:none;
}

El filtrado se realiza con este bucle:

  //-- Filtrar la imagen según el nuevo umbral
    for (let i = 0; i < data.length; i+=4) {
      if (data[i] > umbral)
        data[i] = umbral;
    }

El código completo en javascript es el siguiente:

  • Fichero: Ej-05.js
console.log("Ejecutando JS....")

//-- Obtener elementos del DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('imagesrc');
const ctx = canvas.getContext('2d');

//-- Acceso al deslizador
const deslizador = document.getElementById('deslizador');

//-- Valor del deslizador
const range_value = document.getElementById('range_value');

//-- Función de retrollamada de imagen cargada
//-- La imagen no se carga instantaneamente, sino que
//-- lleva un tiempo. Sólo podemos acceder a ella una vez
//-- que esté totalmente cargada
img.onload = function () {

  //-- Se establece como tamaño del canvas el mismo
  //-- que el de la imagen original
  canvas.width = img.width;
  canvas.height = img.height;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavía
  ctx.drawImage(img, 0,0);

  console.log("Imagen lista...");
};


//-- Función de retrollamada del deslizador
deslizador.oninput = () => {
  //-- Mostrar el nuevo valor del deslizador
  range_value.innerHTML = deslizador.value;

  //-- Situar la imagen original en el canvas
  //-- No se han hecho manipulaciones todavía
  ctx.drawImage(img, 0,0);

  //-- Obtener la imagen del canvas en pixeles
  let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  //-- Obtener el array con todos los píxeles
  let data = imgData.data

  //-- Obtener el umbral de rojo del deslizador
  umbral = deslizador.value

  //-- Filtrar la imagen según el nuevo umbral
  for (let i = 0; i < data.length; i+=4) {
    if (data[i] > umbral)
      data[i] = umbral;
  }

  //-- Poner la imagen modificada en el canvas
  ctx.putImageData(imgData, 0, 0);
}

console.log("Fin...");

En esta animación lo vemos en acción:

Filtro de grises

Un filtro para eliminar los colores y quedarse sólo con el nivel de grises se hace de una forma parecida.

Hay que obtener los niveles de color y calcular el brillo usando esta ecuación:

brillo = (3 * r + 4 * g + b)/8

Luego hay que asignar este valor a los tres canales (para que todos tengan el mismo)

¡A practicar!

Para orientarte en la realización de la práctica haz los siguientes ejercicios:

Ejercicio 1

Haz los 5 ejemplos mostrados en esta sesión.

Estudia y asimila el código, para comprender cómo funciona.

Guardalos en tu repositorio de prácticas, en la carpeta P4/L11

Ejercicio 2

Implementa una función de filtrado usando los tres umbrales de colores: Rojo, verde y azul obtenidos de los deslizadores.

Al llamar a esta función se obtienen primero los valores de los umbrales de los deslizadores y se aplican a la imagen original.

Configura las funciones de retrollamada de los tres deslizadores para invocar a esta función.

Parte del Ejemplo 5, donde se ha implementando un umbral sólo para el color rojo

Ejercicio 3

Implementa los botones para la selección de los filtro Gris y de Color.

Cada vez que se apriete alguno de ellos se activará la función de filtrado correspondiente y se indicará en una variable de estado cuál es el filtro activo.

El movimiento de los deslizadores invocará sus funciones de retrollamada que llaman al filtro de color (Ejercicio 2).

En este filtro primero se comprueba si el filtro activo es el de color.

En caso de ser así se procede a filtrar por colores.

De lo contrario se ignora.

Todavía no tienes implementado el filtro de grises, sólo el de colores.

Pero ahora sólo funcionará cuando tengas pulsado el botón de selección del filtro de color.

Al apretar el de Gris y luego dar a los deslizadores no tendrán efecto.

Ejercicio 4

Implementa ahora el filtro de grises.

Ejercicio 5

Con esto ya tendrás una versión mínima de la aplicación de filtrado.

Da los últimos retoques añadiendo estilo y dejando la interfaz un poco más bonita, y añadiendo mejoras opcionalmente.

Resumen de tareas a realizar

  • Implementa los ejemplos 1 - 5 de esta sesión y guárdalos en la carpeta P4/L11
  • Haz el ejercicio 2: Filtro de umbral de colores
  • Haz el ejercicio 3: Selección del filtro activo
  • Haz el ejercicio 4: Implementación del filtro de grises
  • Haz el ejercicio 5: Termina la práctica e implementa mejoras opcionalmente

Conclusiones

Al terminar esta práctica ya sabrás cómo procesar imágenes desde Javascrip.

Y ya podrás poner en práctica los conocimientos adquiridos en otras asignaturas sobre filtrado de imágenes.

Página principal


Teoría

Práctica 0: Herramientas

Práctica 1: CV

Práctica 2: Boom

Práctica 3: Tiro parabólico

Práctica 4: The Memory Games

Práctica 5: Simulando la red

Examenes

Curso 2022/2023

  • [2023 - 16 de Marzo: L3210 9:00 - 12:00 Parcial 1 ]
  • [2023 - 22 de Mayo L3208 9:00 - 12:00 : Parcial 2 ]
  • [2023 - 14 de Junio L3208 18:00 - 21:00 : Final. Conv. extraordinaria ]
Clone this wiki locally