Hasta ahora vimos qué es la visión por computadora, qué tipos de problemas existen y cómo se representan digitalmente las imágenes. A partir de este tema empezamos a trabajar con una pregunta mucho más práctica: ¿cómo cargamos y manipulamos imágenes en Python?
En la práctica, casi todo flujo de visión artificial empieza igual: leer una imagen desde disco o desde una cámara, inspeccionar sus dimensiones, convertirla a una estructura adecuada, modificarla y luego visualizarla o guardarla.
Para hacer esto en Python se utilizan principalmente bibliotecas como OpenCV, Pillow y NumPy. En este tema aprenderemos los conceptos y operaciones básicas que forman la base del trabajo posterior.
Aunque existen varias herramientas, tres bibliotecas aparecen constantemente en proyectos de visión por computadora en Python:
En muchos proyectos se combinan las tres. OpenCV aporta velocidad y funciones específicas de visión. Pillow facilita ciertas tareas de manejo de archivos e imágenes. NumPy actúa como base numérica para operar sobre los datos.
Una instalación común en Python puede hacerse con pip. Por ejemplo:
pip install opencv-python pillow numpy matplotlib
También es frecuente usar opencv-python-headless en entornos sin interfaz gráfica, como servidores o notebooks remotos. Más adelante veremos situaciones concretas donde esto puede ser relevante.
OpenCV permite cargar una imagen desde disco con una sola función. Un ejemplo básico es:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
print(imagen.shape)
Si la carga fue exitosa, imagen contendrá un arreglo numérico. Si hubo un problema con la ruta, el archivo o el formato, el valor será None.
Por eso es buena práctica verificar siempre el resultado:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
print("No se pudo cargar la imagen")
else:
print("Imagen cargada correctamente")
Con Pillow, la carga de una imagen suele hacerse así:
from PIL import Image
imagen = Image.open("foto1.jpg")
print(imagen.size)
En este caso, el objeto devuelto no es inmediatamente un arreglo de NumPy, sino una estructura propia de Pillow. Si queremos operar numéricamente sobre ella, normalmente la convertimos:
from PIL import Image
import numpy as np
imagen = Image.open("foto1.jpg")
imagen_np = np.array(imagen)
print(imagen_np.shape)
Esta diferencia es importante: OpenCV trabaja directamente con arreglos, mientras que Pillow trabaja primero con objetos de imagen.
Ambas bibliotecas pueden cargar imágenes, pero hay una diferencia que suele causar confusión:
Esto significa que si cargamos una imagen con OpenCV y luego la mostramos con otra biblioteca que espera RGB, es posible que los colores se vean cambiados. Por eso, a menudo hace falta convertir:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
imagen_rgb = cv2.cvtColor(imagen, cv2.COLOR_BGR2RGB)
Esta diferencia existe por razones históricas: OpenCV nació en un contexto más orientado a visión por computadora y procesamiento de bajo nivel, mientras que Pillow siguió una convención más natural para gráficos e imágenes. Por eso OpenCV heredó BGR y Pillow adoptó RGB. No se unificó después porque cambiar OpenCV rompería muchísimo código, ejemplos y modelos ya existentes. En la práctica, lo correcto es recordar qué convención usa cada biblioteca y convertir cuando haga falta.
Ver una imagen cargada correctamente es una parte importante del flujo de trabajo, porque permite comprobar si los datos están en el formato esperado.
Con OpenCV se puede usar:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
cv2.imshow("Ventana", imagen)
cv2.waitKey(0)
cv2.destroyAllWindows()
Sin embargo, en notebooks o entornos interactivos muchas veces se prefiere usar Matplotlib:
import cv2
import matplotlib.pyplot as plt
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
imagen_rgb = cv2.cvtColor(imagen, cv2.COLOR_BGR2RGB)
plt.imshow(imagen_rgb)
plt.axis("off")
plt.show()
Si usamos OpenCV con Matplotlib, recordar la conversión BGR a RGB es esencial para no mostrar colores incorrectos. (Si comentamos la línea de conversión y ejecutamos otra vez el programa, Matplotlib interpretará la imagen como si estuviera en RGB cuando en realidad OpenCV la dejó en BGR. El resultado será una imagen con colores cambiados: las zonas rojas tenderán a verse azules y las zonas azules tenderán a verse rojas).
Una de las primeras cosas que conviene hacer al cargar una imagen es revisar su forma y su tipo de dato:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
print(imagen.shape)
print(imagen.dtype)
Esto puede devolver algo como:
(480, 640, 3) para la forma.uint8 para el tipo de dato.La forma indica alto, ancho y cantidad de canales. El tipo de dato indica cómo están almacenados los valores numéricos.
Como las imágenes son arreglos, podemos acceder a píxeles individuales mediante índices. En OpenCV o NumPy, por ejemplo:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
pixel = imagen[100, 200]
print(pixel)
Si la imagen es en color, el resultado será un vector con tres componentes. Si es en grises, será un único valor.
Esto corresponde al píxel ubicado en la fila 100 y la columna 200.
También podemos alterar directamente los valores de la imagen. Por ejemplo:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
imagen[100, 200] = [0, 0, 255]
cv2.imwrite("pixel_modificado.jpg", imagen)
Si la imagen está en BGR, ese píxel pasará a ser rojo puro en la convención de OpenCV. Este tipo de acceso directo es útil para experimentos, aunque en imágenes grandes conviene evitar modificaciones píxel a píxel con bucles lentos y preferir operaciones vectorizadas.
Una tarea muy frecuente es recortar una parte de la imagen. Esto se conoce como obtener una región de interés o ROI.
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
recorte = imagen[50:200, 100:300]
cv2.imwrite("recorte.jpg", recorte)
Este ejemplo extrae las filas desde 50 hasta 199 y las columnas desde 100 hasta 299. El resultado es una nueva vista o subimagen que puede analizarse por separado.
Los recortes son fundamentales en tareas como detección, segmentación localizada, análisis facial o procesamiento de documentos.
Cuando recortamos con slicing en NumPy, muchas veces no obtenemos una copia independiente, sino una vista sobre los mismos datos. Esto significa que modificar el recorte puede modificar la imagen original.
Si necesitamos una copia independiente, podemos usar:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
recorte = imagen[50:200, 100:300].copy()
cv2.imwrite("recorte_copia.jpg", recorte)
Comprender esta diferencia evita errores difíciles de detectar durante el procesamiento.
Muchas tareas requieren cambiar el tamaño de una imagen. Esto puede ser necesario para:
Con OpenCV, una forma común es:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
redimensionada = cv2.resize(imagen, (300, 200))
cv2.imwrite("redimensionada.jpg", redimensionada)
Aquí el nuevo tamaño se da como (ancho, alto), algo que conviene recordar porque no sigue la misma convención que el acceso por filas y columnas.
Cuando redimensionamos, muchas veces conviene conservar la relación de aspecto para evitar deformaciones. Por ejemplo:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
alto, ancho = imagen.shape[:2]
nuevo_ancho = 400
nuevo_alto = int(alto * nuevo_ancho / ancho)
redimensionada = cv2.resize(imagen, (nuevo_ancho, nuevo_alto))
cv2.imwrite("redimensionada_proporcional.jpg", redimensionada)
Si no respetamos la proporción, los objetos pueden quedar estirados o comprimidos, lo que a veces afecta el análisis visual y el rendimiento de un modelo.
Es muy común convertir imágenes a grises para simplificar operaciones posteriores. Con OpenCV:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
gris = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)
cv2.imwrite("gris.jpg", gris)
Esto transforma una imagen de tres canales en una imagen de un solo canal. Muchas técnicas clásicas de procesamiento trabajan mejor o más rápido sobre este tipo de representación.
Otra operación frecuente es separar los canales de una imagen para analizarlos de forma individual:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
b, g, r = cv2.split(imagen)
print(b.shape, g.shape, r.shape)
Esto permite inspeccionar qué componente resalta mejor una región o aplicar procesamiento específico sobre cada canal.
También es posible volver a unirlos:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
b, g, r = cv2.split(imagen)
imagen_unida = cv2.merge([b, g, r])
cv2.imwrite("imagen_unida.jpg", imagen_unida)
Algunas manipulaciones básicas cambian la orientación de la imagen. Por ejemplo, podemos voltear horizontal o verticalmente:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
horizontal = cv2.flip(imagen, 1)
vertical = cv2.flip(imagen, 0)
Estas operaciones son útiles tanto en preprocesamiento como en data augmentation.
Después de procesar una imagen, es habitual guardarla en disco. Con OpenCV se hace así:
import cv2
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
cv2.imwrite("salida.jpg", imagen)
El formato de salida suele inferirse a partir de la extensión del archivo. Esto permite exportar resultados intermedios, visualizaciones o imágenes transformadas.
En muchos proyectos se mezclan Pillow y NumPy. Una conversión común es:
from PIL import Image
import numpy as np
imagen_pil = Image.open("foto1.jpg")
imagen_np = np.array(imagen_pil)
print(imagen_np.shape)
Y si queremos volver desde NumPy a Pillow:
from PIL import Image
import numpy as np
imagen_np = np.zeros((200, 300, 3), dtype=np.uint8)
imagen_np[:, :] = [255, 0, 0]
imagen_pil = Image.fromarray(imagen_np)
imagen_pil.save("imagen_desde_numpy.png")
Esto es útil cuando una biblioteca ofrece una función conveniente pero el resto del pipeline trabaja con otro formato interno.
Cuando trabajamos con imágenes en Python conviene aprovechar las operaciones vectorizadas de NumPy en lugar de recorrer píxeles con bucles explícitos. Por ejemplo, si queremos aclarar una imagen:
import cv2
import numpy as np
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
aclarada = np.clip(imagen + 30, 0, 255).astype(np.uint8)
cv2.imwrite("aclarada.jpg", aclarada)
Este enfoque es mucho más eficiente que modificar cada píxel uno por uno en un for.
Una fuente común de errores aparece al operar con imágenes uint8. Como estos valores están limitados entre 0 y 255, ciertas operaciones pueden producir desbordamientos si no se controlan bien.
Por eso es frecuente usar:
np.clip para limitar valores.uint8 al final.Este detalle parece menor, pero es clave para evitar resultados numéricos inesperados.
En programas reales conviene manejar correctamente las rutas de las imágenes. Una mala ruta es una de las causas más frecuentes de error al cargar archivos.
En Python suele ser recomendable usar pathlib:
from pathlib import Path
import cv2
ruta = Path("imagenes") / "foto1.jpg"
imagen = cv2.imread(str(ruta))
if imagen is None:
raise ValueError(f"No se pudo cargar la imagen: {ruta}")
Esto ayuda a escribir código más claro y portable.
Cuando trabajamos con datasets, no suele alcanzarnos con cargar una sola imagen. Es común recorrer directorios completos:
from pathlib import Path
import cv2
for ruta in Path("imagenes").glob("*.jpg"):
imagen = cv2.imread(str(ruta))
if imagen is None:
continue
print(ruta.name, imagen.shape)
Este tipo de lectura es la base de pipelines de entrenamiento, validación y análisis exploratorio.
Un flujo práctico y simple de manipulación de imágenes suele seguir estos pasos:
Este patrón se repite muchísimas veces en proyectos reales.
Un ejemplo sencillo combinando varias operaciones podría ser:
import cv2
import matplotlib.pyplot as plt
imagen = cv2.imread("foto1.jpg")
if imagen is None:
raise ValueError("No se pudo cargar la imagen")
gris = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)
redimensionada = cv2.resize(gris, (300, 200))
plt.imshow(redimensionada, cmap="gray")
plt.axis("off")
plt.show()
En este código:
Al comenzar a manipular imágenes en Python, es frecuente cometer algunos errores típicos:
None.shape y dtype.uint8.Evitar estos problemas desde el principio mejora mucho la estabilidad del código y la interpretación de resultados.
La carga y manipulación de imágenes en Python es la puerta de entrada a la práctica real de la visión por computadora. Antes de aplicar filtros avanzados, entrenar redes neuronales o construir detectores, primero hay que dominar operaciones básicas como leer, inspeccionar, convertir, recortar y guardar imágenes.
Estas tareas parecen simples, pero forman la infraestructura cotidiana de casi cualquier proyecto visual. Cuanto más sólidas sean estas bases, más fácil será avanzar hacia procesamiento clásico y Deep Learning.
En el próximo tema veremos las librerías fundamentales para visión por computadora, ordenando mejor el ecosistema de herramientas que usaremos durante el curso.