Twitter Facebook RSS Feed

domingo, 05 de febrero de 2023 a las 16:53hs por Gustavo Cantero (The Wolf)

Hace unos meses tuvimos que desarrollar una aplicación que compara capturas de sitios web, para saber si habían cambiado de un día al siguiente. Esto suena sencillo, se puede comparar píxel por píxel, y si todos son iguales las capturas son iguales. El problema está cuando los sitios tienen alguna pequeña diferencia, por ejemplo, si tiene fecha y hora en algún lugar, algún banner que cambie o rote, la compresión JPEG puede hacer variar algunos píxeles (esto se puede solucionar utilizando PNG para las capturas, pero haría que las mismas fuera mucho más grandes), etc. Para solucionar esto utilizamos OpenCV, gracias al cual pudimos establecer un porcentaje de similitud.

¿Qué librería utilizar?

Lo primero con lo que nos topamos es que nuestra aplicación está hecha en .net con C#, por lo cual, no podíamos utilizar directamente OpenCV porque es una librería para C++ que utiliza el GPU a través de OpenCL y CUDA, portada a algunos lenguajes como Python y JavaScript, pero no para .net, entonces, debemos crear todos los objetos en .net «referenciando» a los objetos de las librerías de C++, o podemos utilizar alguna librería de .net que ya haga esto. Obviamente, optamos por la segunda opción.
Entre las opciones que analizamos están Emgu CV y OpenCvSharp4. Comenzamos utilizando la primera, porque tiene más documentación en inglés (la otra tiene mucha de su documentación en japonés).
Usando Emgu CV portamos varios ejemplos de C++ y python a .net, pero algunos no pudimos porque la librería estaba incompleta y no tenía todos los objetos de OpenCV, por lo que finalmente decidimos usar OpenCvSharp4.
Como OpenCV está disponible para varias plataformas, al igual que .net, además de instalar la librería OpenCvSharp4 debemos instalar la librería para el sistema operativo donde lo vamos a ejecutar, y como en nuestro caso es una aplicación para Windows, agregamos la librería OpenCvSharp4.runtime.win.

Ahora si, ya con la librería que vamos a utilizar, podemos escribir el código que realizará la comparación de las imágenes.
Lo primero que debemos hacer, como explicamos en el punto anterior, es agregar las librerías al proyecto, lo cual realizaremos utilizando nuget.

Ahora si, a programar

La idea de la comparación será binarizar las imágenes, o sea generar imágenes en blanco y negro, para luego compararlas y contar los píxeles distintos.
Para eso lo primero que tenemos que hacer es leer las imágenes, pero como vamos a binarizarlas utilizando la función Threshold las necesitamos en escala de grises. La lectura de las imágenes la haremos con la clase Mat, que representa un array de varias dimensiones, y le especificaremos que lo queremos en escale de grises.

using Mat sourceImage = Mat.FromStream(file1.FileContent, ImreadModes.Grayscale);

Luego tomaremos cada imagen y la binarizamos, como dijimos antes, con el método Threshold. A este método tenemos que pasarle la imagen de origen, un objeto donde guardar el resultado, el threshold y el valor máximo a utilizar en el algoritmo, que especificaremos con 0 y 255 respectivamente, y el tipo de algoritmo a usar. En OpenCV hay varios tipos de algoritmos que podemos utilizar, los cuales se muestran en la siguiente imagen:

Nosotros utilizaremos Binary y le agregaremos la opción Triangle para que utilice valores de óptimos del threshold.

using Mat sourceImageGauss = new();
Cv2.Threshold(sourceImage, sourceImageGauss, 0, 255, ThresholdTypes.Triangle | ThresholdTypes.Binary);

Esta operación la hacemos con ambas imágenes.
Ahora, ya con las imágenes binarizadas, creamos una nueva imagen con la «diferencia» entre ambos, a través del método Absdiff.

using Mat resultImage = new();
Cv2.Absdiff(image1, image2, resultImage);

Ya con la imagen resultante podemos contar la cantidad de píxeles blancos que hay en el resultado con el método CountNonZero.

int diff = Cv2.CountNonZero(resultImage);

Juntando todo lo anterior, podemos escribir este código de ejemplo:

using Mat sourceImage = Mat.FromStream(file1.FileContent, ImreadModes.Grayscale);
using Mat sourceImage2 = Mat.FromStream(file2.FileContent, ImreadModes.Grayscale);

int width = sourceImage.Width;
int height = sourceImage.Height;

if (width != sourceImage2.Width || height != sourceImage2.Height)
    throw new Exception("Las imágenes deben tener el mismo tamaño");

using Mat sourceImageGauss = new();
Cv2.Threshold(sourceImage, sourceImageGauss, 0, 255, ThresholdTypes.Triangle | ThresholdTypes.Binary);

using Mat sourceImage2Gauss = new();
Cv2.Threshold(sourceImage2, sourceImage2Gauss, 0, 255, ThresholdTypes.Triangle | ThresholdTypes.Binary);

using Mat resultImage = new();
Cv2.Absdiff(sourceImage2Gauss, sourceImageGauss, resultImage);

double diff = Cv2.CountNonZero(resultImage);
double similarity = 1 - (diff / (width * height));

Ejemplo

Para mostrar el funcionamiento de esto, hice un pequeño ejemplo que les dejo en github .
En ese proyecto muestro una página que permite seleccionar dos imágenes (deben tener las mismas dimensiones), las binariza, las compara y muestra los resultados.
Como ejemplo, busqué una imagen por internet del clásico juego de «encuentra las diferencias»:

Si estas imágenes las comparamos con la aplicación, devolverá lo siguiente en menos de un tercio de segundo:

Se izquierda a derecha se ven la primer imagen binarizada, la segunda y la diferencia entre ambas.
En la tercer imagen se aprecian los bordes de los objetos, porque las imágenes no son exactamente iguales, y más resaltadas las diferencias.
Espero que les sea de utilidad.
¡Suerte!

0 comentarios »

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.