Skip to content

A-NullPointer/VC-P2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Práctica 2

Asignatura: Visión por Computador

Universidad de Las Palmas de Gran Canaria
Escuela de Ingeniería en Informática
Grado de Ingeniería Informática
Curso 2025/2026

Autores

  • Asmae Ez Zaim Driouch
  • Javier Castilla Moreno

Bibliotecas utilizadas

NumPy OpenCV Matplotlib Pillow Tkinter

Cómo usar

Primer paso: clonar este repositorio

git clone "https://github.com/A-NullPointer/VC-P2"

Segundo paso: Activar tu envinroment e instalar dependencias

Note

Todas las dependencias pueden verse en este archivo. Si se desea, puede crearse un entorno de Conda con dicho archivo.

Si se opta por crear un nuevo Conda envinronment a partir del archivo expuesto, es necesario abrir el Anaconda Prompt y ejecutar lo siguiente:

conda env create -f environment.yml

Posteriormente, se activa el entorno:

conda activate VC_P2

Tercer paso: ejecutar el cuaderno

Finalmente, abriendo nuestro IDE favorito y teniendo instalado todo lo necesario para poder ejecutar notebooks, se puede ejecutar el cuaderno de la práctica Practica2.ipynb seleccionando el envinronment anteriormente creado.

Important

Todos los bloques de código deben ejecutarse en órden, de lo contrario, podría ocasionar problemas durante la ejecución del cuaderno.

Tareas

Tarea 1 CANNY: Contar píxeles no nulos en cada fila haciendo un conteo de aquellas que tienen un valor obtenido mayor o igual al 90% del máximo usando

Se aplica Canny para detectar bordes en la imagen en escala de grises:

gris = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gris, 100, 200)

Se normaliza el conteo de píxeles blancos dividiendo por el valor máximo del píxel (255) y el número de columnas:

row_counts = cv2.reduce(canny, 1, cv2.REDUCE_SUM, dtype=cv2.CV_32SC1).flatten()
row = row_counts / (255 * canny.shape[1])

Se determina el valor máximo de píxeles blancos por fila y se guarda en una variable las filas que superan el 90% de este máximo:

umbral = 0.9 * max_row
filas_seleccionadas = np.where(row >= umbral)[0]

Las filas seleccionadas se marcan en rojo sobre la imagen de Canny para visualizar las regiones con mayor concentración de bordes horizontales.

De manera similar, se realiza el análisis por columnas:

col_counts = cv2.reduce(canny, 0, cv2.REDUCE_SUM, dtype=cv2.CV_32SC1)
cols = col_counts[0] / (255 * canny.shape[0])

Resultados

Los histogramas muestran la distribución de bordes detectados:

Tarea 2: Aplicar umbralizado a imagen resulante de sobel. Posteriormente, contar filas y columnas con píxeles no nulos, remarcando aquellas que tengan un valor mayor al 90% del máximo. Canny vs Sobel

Para realizar esta tarea, se ha usado la misma imagen del mandril leída de disco anteriormente para posteriormente aplicar Sobel y a continuación umbralizar la imagen.

gaussian_image = cv2.GaussianBlur(cv2.cvtColor(image, cv2.COLOR_RGB2GRAY), (3, 3), 0)
sobel_y = cv2.Sobel(gaussian_image, cv2.CV_64F, 0, 1)
sobel_x = cv2.Sobel(gaussian_image, cv2.CV_64F, 1, 0)
sobel = cv2.convertScaleAbs(cv2.add(sobel_x, sobel_y))

Primero se ha aplicado un filtro Gaussiano, que nos difuminará ligeramente la imagen eliminando el ruido para posteriormente, aplicar Sobel en vertical y horizontal. Finalmente se juntan los resultados y se obtiene la imagen de Sobel con bordes verticales y horizontales.

Important

Es necesario pasar el resultado a 8 bits, de los contrario, los valores de los píxeles podrían salirse del rango (0, 255).

A continuación se muestra el resultado:

Sobel horizontal

Sobel vertical

Sobel combinado

Posteriormente, para obtener el umbral necesario, se ha calculado el histograma de esta imagen a la que se le ha aplicado Sobel con ayuda de la función cv2.calcHist de OpenCV:

histogram = cv2.calcHist([cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)], [0], None, [256], [0, 256])

Y este es el histograma resultante:

En el valle situado entre los dos picos del histograma, podemos encontrar el umbral necesario.

Note

Puede sumarse cv2.THRES_OTSU al realizar el umbralizado con ayuda de OpenCV para obtener un umbral de manera automática. Posteriormente será utilizado en el demostrador. Sin embargo, en este punto se ha decidio utilizar un valor seleccionado a ojo en ese valle.

Una vez obtenemos el umbral que necesitábamos, aplicamos el umbralizado a la imagen de sobel con la función cv2.thresold:

threshold = 130

_, threshold_image = cv2.threshold(cv2.cvtColor(image, cv2.COLOR_RGB2GRAY), threshold, 255, cv2.THRESH_BINARY)

El resultado obtenido es el siguiente:

Tras umbralizar la imagen, podemos seguir con la tarea. Ahora, se intenta contar las filas y columnas con valores no nulos de la imagen resultante para posteriormente, marcar en la misma aquellas que superen el 90% del máximo de píxeles encontrados en una fila / columna.

Para lograr lo descrito anteriormente, se ha recurrido a la función np.count_nonzero de Numpy, la cual es perfecta para esta situación, pues nos dará un array donde cada valor corresponde al número de píxeles no nulos en dicha fila / columna. La función desarrollada para tal fin es la siguiente:

def statistics(image, p, axis):
    counts = np.count_nonzero(image, axis=axis)
    max_index = np.argmax(counts)
    max_value = counts[max_index]
    threshold = p*max_value
    return counts, max_index, max_value, {i: int(counts[i]) for i in range(len(counts)) if counts[i] > threshold}

A la función le pasamos como parámetros la propia imagen, el porcentaje objetivo y el eje (filas / columnas).

Note

Para encontrar la posición y el valor máximo se ha hecho uso de la función np.argmaxde Numpy, la cual devuelve el índice del elemento con el valor máximo dentro del array.

Seguidamente, hacemos el conteo de aquellas filas y columnas con valores por encima del 90% del máximo:

counts_rows_sobel, max_row_index_sobel, max_row_value_sobel, rows_sobel = statistics(threshold_image_sobel, .9, 1)
counts_columns_sobel, max_column_index_sobel, max_column_value_sobel, columns_sobel = statistics(threshold_image_sobel, .9, 0)

Los resultados obtenidos son los siguientes:

-------------Filas-------------
La fila con índice 82 tiene el valor máximo con 161 píxeles blancos
Las filas que tienen un valor mayor al 90% del máximo son las siguientes:
	Fila 3 con valor 155
	Fila 4 con valor 149
	Fila 20 con valor 149
	Fila 51 con valor 151
	Fila 81 con valor 152
	Fila 82 con valor 161
	Fila 83 con valor 154
------------Columnas-----------
La columna con índice 288 tiene el valor máximo con 179 píxeles blancos
Las columnas que tienen un valor mayor al 90% del máximo son las siguientes:
	Columna 288 con valor 179

Note

Los datos extras no utilizados actualmente nos servirán en la comparación posterior con Canny.

Tras obtener estos datos, podemos remarcar dichos ejes que cumplan la condición planteada en esta tarea usando primitivas gráficas de OpenCV del siguiente modo:

threshold_image_marked = cv2.cvtColor(threshold_image_sobel, cv2.COLOR_GRAY2RGB)

draw_rows_and_columns(threshold_image_marked, rows_sobel, columns_sobel)

La función utilizada para dibujar dichas primitivas gráficas es la siguiente:

def draw_rows_and_columns(image, rows, columns):
    h, w, *_ = image.shape
    for row in rows.keys():
        cv2.line(image, (0, row), (w, row), (255, 0, 0), 1)

    for col in columns.keys():
        cv2.line(image, (col, 0), (col, h), (0, 255, 0), 1)

A continuación se muestra el resultado:

Finalmente, lo compararemos con Canny. Se han seguido los mismos procedimientos que en el caso de Sobel y este es el resultado:

Como se puede observar, en el caso de Canny es donde más filas y columnas se han remarcado, y también es donde mayor número de píxeles hay. A continuación, se muestra la comparación gráfica de ambos casos para el conteo de sus filas y columnas con valores no nulos:

Con estas gráficas, puede apreciarse mucho mejor como Canny posee un mayor número de píxeles no nulos en cada fila y columna en comparación con Sobel.

Tarea 3: Demostrador

Para el desarrollo de este demostrador, se ha reutilizado la clase desarrollada en la práctica anterior para aplicar transformaciones a los canales de una imagen, ampliando sus métodos para añadir nuevas formas de modificar la imagen con conceptos aprendidos en estas dos prácticas. Además, se ha englobado todo ello en una pequeña aplicación usando la biblioteca gráfica de python, Tkinter.

Los nuevos métodos son los siguientes:

@staticmethod
def canny_inverted(image, *args):
    return Demo.negative_image(cv2.Canny(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY), 50, 200))
    
@staticmethod
def sobel(image, *args):
    gaussian_image = cv2.GaussianBlur(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY), (3, 3), 0)
    sobel_y = cv2.Sobel(gaussian_image, cv2.CV_64F, 0, 1)
    sobel_x = cv2.Sobel(gaussian_image, cv2.CV_64F, 1, 0)
    return cv2.convertScaleAbs(cv2.add(sobel_x, sobel_y))
    
@staticmethod
def isolate_color(image, mask1, mask2):
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, mask1, mask2)
    color_part = cv2.bitwise_and(image, image, mask=mask)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray_bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
    return np.where(mask[:, :, None] != 0, color_part, gray_bgr)
    
@staticmethod
def threshold_function(image, *args):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, threshold_image = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    return threshold_image

@staticmethod
def create_colored_edges_effect(frame, *args):
    colored = Demo.posterize(frame, levels=6)
    colored = Demo.apply_pop_art_color(colored, hue_shift=30)
    edges = Demo.border_canny(frame, 100, 255)
    edges_inv = cv2.bitwise_not(edges)
    edges_color = cv2.cvtColor(edges_inv, cv2.COLOR_GRAY2BGR)
    result = cv2.bitwise_and(colored, edges_color)
    return result

@staticmethod
def create_cartoon_effect(frame, *args):
    bits = 1
    levels = 2 ** bits
    step = 256 // levels
    colored_regions = (frame // step) * step + step // 2  
    edges = Demo.border_canny_grosor(frame, linf=100, lsup=255, grosor=2)
    mask_edges_bgr = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
    mask = mask_edges_bgr.astype(float) / 255.0

    #result = cv2.add(colored_regions, edges_bgr)
    result = (colored_regions * mask).astype(np.uint8)

    return result

En concreto, se han añadido utilizades para:

  • Hacer un collage con inversión completa de los colores de la imagen e inversión individual de cada canal de la misma
  • Aplicar Canny a cada fotograma e invertir la misma, consiguiendo un efecto de dibujo
  • Aplicar Canny a cada fotograma, mostrando los bordes con su respectivo color de la imagen original
  • Resaltar los objetos con valores HSV seleccionados, dejando el resto de la imagen en escala de grises
  • Aplicar Sobel a cada fotograma
  • Aplicar umbralizado a cada fotograma
  • Efecto Cartoon aplicando Canny y disminución de bits en cada canal

Note

Los controles de la aplicación pueden verse en la misma ventana de ejecución. El modo actual será mostrado justo encima del vídeo. Ademaás, en la zona derecha de la ventana pueden verse los controles para modificar los valores de los rangos para HSV.

A continuación, se muestran todos los modos disponibles en funcionamiento:

Collage de inversiones

Canny y efecto dibujo

Canny colorido

HSV y rangos 1

HSV y rangos 2

Sobel para efecto relieve

Umbralizado

Efecto Cartoon

Note

El código de la aplicación puede verse en el cuaderno Practica2.ipynb.

Tarea 4: Reinterpretaciones

Con el fin de fomentar el aprendizaje y dejar volar nuestra imaginación, hemos decidido que cada miembro del grupo desarolle su propia interpretación. Por ello, se ha realizado un Pincel Virtual (Tarea 4a) y Pintar con Movimiento (Tarea 4b). A continuación, se presentara la primera parte de esta tarea.

Tarea 4a: Reinterpretación de Air Guitar: Pincel Virtual

En concreto, se ha seleccionado el color rojo para ser nuestro pincel virtual. La pequeña aplicación desarrollada en Tkinter permite capturar la imagen desde la webcam del ordenador para posteriormente, con ayuda de nuestro rotulador de tapa roja, empezar a dibujar sobre dicha imagen capturada.

Note

Para desarrollar el pincel virtual, se ha hecho uso del espacio de colores HSV, el cual permite, con ayuda de funciones de OpenCV, detertar rangos de colores con el fin de aislar o detectar los mismos en cada fotograma.

A la hora de realizar esta interpretación surgió un problema, y era cómo detectar la posición del objeto rojizo en la imagen. Investigando en la documentación de OpenCV, se ha encontrado una función capaz de encontrar contornos y posteriormente, encerrarlo en un rectángulo, obteniendo las coordenadas X e Y del mismo así como su altura y ancho. Dichas funciones son cv2.findContours y cv2.boundingRect respectivamente. Con esto, se tiene todo lo necesario para empezar a simular un pincel virtual detectando un objeto rojizo.

Note

Dependiendo de la altura del objeto usado como pincel, el grosor de la pincelada virtual aumentará o disminuirá. Además, la aplicación permite observar la imagen tomada originalmente sobre la que también se superpondrá el objeto detectado como pincel, qué es lo que se está detectando que podría ser detectado como el pincel virtual, el lienzo resultante de las pinceladas, y la combinación del lienzo sobre la imagen original. Con todo esto, la diversión siempre estará presente.

A continuación, se muestran varios ejemplos de uso de esta interpretación realizada:

Note

Se pueden borrar los trazos realizados con la tecla BACKSPACE. El código de la aplicación puede verse en el cuaderno Practica2.ipynb.

Tarea 4b: Estela con movimiento

Inspirado en la instalación interactiva "Messa di Voce", esta demo pinta el movimienti detectado por cámara resaltando los bordesy según la intensidad se dejará una estela de diferentes colores. Una vez no hay movimiento los trazos se desvanecen.

Detección de movimiento

def detectar_movimiento(frame_actual, frame_prev, umbral=25):
    gris_actual = cv2.cvtColor(frame_actual, cv2.COLOR_BGR2GRAY)
    gris_prev = cv2.cvtColor(frame_prev, cv2.COLOR_BGR2GRAY)
    
    gris_actual = cv2.GaussianBlur(gris_actual, (5, 5), 0)
    gris_prev = cv2.GaussianBlur(gris_prev, (5, 5), 0)
    
    diff = cv2.absdiff(gris_actual, gris_prev)
    _, mask = cv2.threshold(diff, umbral, 255, cv2.THRESH_BINARY)
    
    kernel = np.ones((5, 5), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
    
    return mask, diff
  1. Convierte ambos frames a escala de grises
  2. Aplica suavizado Gaussiano para reducir ruido
  3. Calcula la diferencia absoluta entre frames
  4. Umbraliza para obtener máscara binaria de movimiento
  5. Aplica operaciones morfológicas para limpiar ruido

Generación de colores

def aplicar_colores_movimiento(mask, intensidad, frame_shape):
    h, w = frame_shape[:2]
    
    global contador_frames
    hue = (contador_frames % 180)
    
    color_frame = np.zeros((h, w, 3), dtype=np.uint8)
    color_frame[:, :, 0] = hue  # Hue
    color_frame[:, :, 1] = 255  # Saturación máxima
    color_frame[:, :, 2] = np.clip(intensidad * 3, 0, 255).astype(np.uint8)
    
    mask_3d = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
    color_frame = cv2.bitwise_and(color_frame, mask_3d)
    
    color_bgr = cv2.cvtColor(color_frame, cv2.COLOR_HSV2BGR)
    return color_bgr

El color cambia cíclicamente a través del espectro HSV

Detección de bordes del movimiento

def detectar_bordes_movimiento(mask):
    bordes = cv2.Canny(mask, 50, 150)
    
    kernel = np.ones((3, 3), np.uint8)
    bordes = cv2.dilate(bordes, kernel, iterations=1)
    
    return bordes

Se aplica Canny sobre la máscara de movimiento y se dilatan los bordes para hacerlos más visibles en color blanco brillante.

Actualización del canvas

def procesar_frame(frame, frame_prev):
    global canvas, contador_frames
    
    if canvas is None:
        canvas = np.zeros((h, w, 3), dtype=np.uint8)
    
    mask_movimiento, diff = detectar_movimiento(frame, frame_prev, UMBRAL_MOVIMIENTO)
    intensidad = calcular_intensidad_movimiento(diff)
    color_movimiento = aplicar_colores_movimiento(mask_movimiento, intensidad, frame.shape)
    
    bordes = detectar_bordes_movimiento(mask_movimiento)
    bordes_bgr = cv2.cvtColor(bordes, cv2.COLOR_GRAY2BGR)
    bordes_bgr[bordes > 0] = [255, 255, 255]
    
    canvas = (canvas * DECAY_RATE).astype(np.uint8)
    canvas = cv2.add(canvas, color_movimiento)
    canvas = cv2.add(canvas, bordes_bgr)
    
    contador_frames += 2
    return canvas

El canvas se actualiza aplicando una multiplicación por la variable DECAY_RATE (0.95). Se aplica los bordes en blanco para el movimiento y la adicción de color.

Controles interactivos

  • ESC: Salir
  • SPACE: Limpiar canvas
  • + / -: Ajustar sensibilidad del movimiento
  • d / D: Ajustar velocidad de desvanecimiento

Bibliografía

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors