Volver a la lista

Cuantificación de Western blot: medición reproducible de la intensidad de las bandas a partir de arreglos de imágenes de NumPy.

Medición automática de la intensidad de las bandas de Western blot en imágenes de gel mediante NumPy y normalización con el control de carga GAPDH. Un flujo de trabajo práctico para sustituir ImageJ con Python.

Intermedio
|
90min
|
Verificado (2026-07)
Western blotdensitometryband quantificationloading controlGAPDH normalizationprotein quantificationROI selection
Progreso0/12 (0%)

Cuantificación de Western Blot: medición reproducible de la intensidad de bandas a partir de arreglos de imágenes NumPy

Al finalizar este tema

Podrán crear una herramienta que automatice la medición de la intensidad de las bandas en imágenes de geles de Western Blot y su normalización mediante un control de carga, integrando los arreglos NumPy, la correlación y el manejo de excepciones aprendidos en el libro de texto. Esto sustituye las acciones de clic en ImageJ por scripts de Python para garantizar la reproducibilidad.

Este artículo es un ejemplo general con fines educativos. La densitometría en la práctica requiere procesos más sofisticados, como la detección automática de bandas, la corrección del fondo y la detección de saturación.


"100 clics en ImageJ": las trampas de la cuantificación manual

La forma en que probablemente aprendieron a cuantificar Western Blot en su tercer año de posgrado suele ser la siguiente:

  1. Abrir la imagen en ImageJ
  2. Usar la herramienta Rectángulo para delimitar una banda
  3. Analyze > Measure clic
  4. Copiar el valor IntDen de la ventana de resultados
  5. Pegarlo en Excel
  6. Repetir con la siguiente banda...

Para un solo gel, son 8 carriles × 3 anticuerpos = 24 veces. Si hay 10 geles, son 240 veces. Las manos se cansan y surgen errores.

Problemas prácticos de este enfoque:

Problema 1: Reproducibilidad. No queda registro de la posición exacta del rectángulo que dibujaron. Si un revisor pregunta "¿cómo midieron esta intensidad de banda?", no tendrán forma de responder.

Problema 2: Subjetividad en la corrección del fondo. El valor final de intensidad varía considerablemente dependiendo de dónde se defina el fondo. Es difícil aplicar siempre el mismo valor en las opciones de sustracción de fondo de ImageJ.

Problema 3: Escala. Si el día anterior a la entrega del artículo se agregan 20 imágenes de geles más, el trabajo manual resulta inmanejable.

El enfoque real consiste en diseñar un pipeline de cuantificación con scripts de Python. Se especifican las coordenadas de cada carril, se calcula la suma de píxeles del área de la banda con NumPy, se normaliza con el control de carga y se guardan los resultados en CSV. Con exactamente el mismo script se obtienen exactamente los mismos resultados de forma reproducible.


De la caja negra a los componentes

Componente 1: Imagen = arreglo 2D de NumPy

La imagen del gel es un arreglo 2D donde cada píxel tiene un valor de brillo. En Python, se carga con PIL o imageio y se convierte en un arreglo numpy.

python
import numpy as np
from PIL import Image
def load_blot_image(path: str) -> np.ndarray:
img = Image.open(path).convert("L") # Convertir a escala de grises
return np.array(img)
image = load_blot_image("western_blot_01.png")
print(image.shape) # Ejemplo: (400, 800) — 400 píxeles de alto y 800 de ancho
print(image.dtype) # uint8 — intensidad de 0 a 255

Nota: En imágenes de 8 bits, el brillo varía de 0 a 255. Las bandas oscuras corresponden a valores bajos. En densitometría, generalmente se invierte el valor: en lugar de brillo, se utiliza la densidad = 255 - brillo.

python
density = 255 - image

Componente 2: Selección de la ROI (Región de Interés)

Especifique la región rectangular con bandas mediante coordenadas.

python
def measure_band(density: np.ndarray, roi: tuple[int, int, int, int]) -> float:
"""
roi = (top, left, bottom, right)
Devuelve: suma de la densidad de píxeles de la región (densidad integrada)
"""
top, left, bottom, right = roi
region = density[top:bottom, left:right]
return float(region.sum())
band_intensity = measure_band(density, (100, 200, 130, 260))
print(f"Intensidad de la banda: {band_intensity}")

Esta función calcula exactamente el mismo valor que la densidad integrada (IntDen) de ImageJ.

Parte 3: Corrección de fondo

También se mide el área de fondo cercana a la banda y se resta.

python
def measure_band_with_background(
density: np.ndarray,
band_roi: tuple[int, int, int, int],
bg_roi: tuple[int, int, int, int]
) -> float:
band = measure_band(density, band_roi)
bg_area = (bg_roi[2] - bg_roi[0]) * (bg_roi[3] - bg_roi[1])
band_area = (band_roi[2] - band_roi[0]) * (band_roi[3] - band_roi[1])
bg_density_per_pixel = measure_band(density, bg_roi) / bg_area
background_contribution = bg_density_per_pixel * band_area
return band - background_contribution

Esta función resta la densidad esperada del fondo de la densidad total de la región de la banda. Dado que el fondo puede ser más amplio que la banda, se normaliza por la densidad por píxel y luego se multiplica.

Parte 4: Normalización del control de carga

En Western blot, se normaliza utilizando la intensidad del control de carga (generalmente GAPDH o β-actina). Esto se hace para corregir las pequeñas diferencias en la cantidad total de proteína cargada en cada carril.

python
def normalize_to_control(target: float, control: float) -> float:
if control == 0:
raise ValueError("Loading control cannot be zero")
return target / control

Procesar varias vías simultáneamente:

python
def quantify_gel(
density: np.ndarray,
target_rois: list[tuple[int, int, int, int]],
control_rois: list[tuple[int, int, int, int]],
background_roi: tuple[int, int, int, int]
) -> list[float]:
"""
Mide la intensidad objetivo y la intensidad de control de cada carril
y devuelve los valores normalizados.
"""
if len(target_rois) != len(control_rois):
raise ValueError("Target and control ROIs must have same length")
normalized = []
for target_roi, control_roi in zip(target_rois, control_rois):
target = measure_band_with_background(density, target_roi, background_roi)
control = measure_band_with_background(density, control_roi, background_roi)
normalized.append(normalize_to_control(target, control))
return normalized

Validación práctica — Correlación con el control de carga

Existe un método para verificar si la normalización se realizó correctamente. Si las intensidades del control de carga difieren significativamente entre sí, esto indica que la cantidad cargada en cada carril fue muy distinta. Si se utilizan las intensidades del target sin normalizar, esta diferencia contaminará los resultados.

python
def check_loading_uniformity(control_intensities: list[float]) -> dict:
arr = np.array(control_intensities)
mean = float(arr.mean())
std = float(arr.std())
cv = std / mean if mean > 0 else float("inf")
return {
"mean": mean,
"std": std,
"cv": cv, # coefficient of variation
"acceptable": cv < 0.2 # Un 20 % o menos es un intervalo seguro
}

Si el CV (coeficiente de variación) es superior al 20%, esto indica que la carga del gel fue muy desigual. En este caso, es necesario repetir el experimento o tener precaución al interpretar los resultados incluso después de la normalización.

También se debe verificar la correlación entre el target y el control. Una correlación positiva fuerte significa que las diferencias en la carga dominan las mediciones del target, lo que justifica la necesidad de la normalización.

python
def check_target_control_correlation(
targets: list[float],
controls: list[float]
) -> float:
t_arr = np.array(targets)
c_arr = np.array(controls)
return float(np.corrcoef(t_arr, c_arr)[0, 1])

Un valor de 0.7 o superior indica un fuerte efecto de normalización. Un valor bajo significa que la diferencia en la carga no es significativa, por lo que la normalización no afecta considerablemente los resultados.


Desvanecimiento: los tres espacios en blanco que debes completar

Espacio en blanco 1: Detección automática de carriles

En lugar de especificar manualmente la posición de los carriles, se realiza una detección automática de carriles analizando el borde inferior de la imagen mediante un perfil de brillo.

python
def detect_lanes(density: np.ndarray, num_lanes: int) -> list[int]:
"""
Calcula la suma de densidad en cada posición vertical de la imagen
y devuelve las posiciones de los picos como centros de los carriles.
"""
column_profile = density.sum(axis=0)
# TODO: localizar los num_lanes picos principales y devolver una lista ordenada de sus posiciones
# Pista: se puede usar scipy.signal.find_peaks
pass

Pista: from scipy.signal import find_peaks; peaks, _ = find_peaks(column_profile, distance=min_lane_spacing).

Espacio en blanco 2: Detección de saturación de banda

Si la banda es demasiado intensa y los píxeles alcanzan el valor 255, no se puede determinar la intensidad real.

python
def check_saturation(
image: np.ndarray,
band_roi: tuple[int, int, int, int],
saturation_threshold: float = 0.05
) -> bool:
"""
Proporción de píxeles de la ROI cuyo valor es 0 (= banda totalmente oscura; 255 al invertirla).
Se considera saturada si es mayor o igual que threshold.
"""
top, left, bottom, right = band_roi
region = image[top:bottom, left:right]
# TODO: calcular la proporción de píxeles de region cuyo valor sea menor o igual que 0
# Compararla con threshold y devolver un bool
pass

Pista: saturated_pixels = (region <= 0).sum(); ratio = saturated_pixels / region.size.

Espacio en blanco 3: Pipeline de cuantificación con seguridad ante excepciones

Al procesar varias imágenes de geles por lotes, si una imagen falla, las demás deben continuar procesándose.

python
def batch_quantify(
image_paths: list[str],
roi_config: dict
) -> dict:
"""
Cuantifica cada imagen y registra los fallos junto con su error.
Devuelve: {"results": [...], "errors": [...]}
"""
results = []
errors = []
for path in image_paths:
try:
# TODO: cargar la imagen, cuantificarla y añadir el resultado con append
pass
except Exception as e:
# TODO: registrar en errors qué archivo produjo qué error
pass
return {"results": results, "errors": errors}

Sugerencia: Utilice except (FileNotFoundError, ValueError) as e: para capturar varios tipos de excepciones o capture la excepción de nivel superior Exception, pero con registros detallados.


Reflexión — Diferencias con las herramientas de densitometría profesionales

Precisión subpíxel: Las herramientas profesionales determinan el centro de la banda con precisión subpíxel. Su ROI se define en unidades de píxeles enteros.

Fondo con gradiente: El fondo de las imágenes de gel suele ser no uniforme y presenta gradientes. Las herramientas profesionales eliminan el fondo con gradiente mediante algoritmos como la sustracción de fondo con bola rodante (rolling ball background subtraction). Es una de las opciones predeterminadas en ImageJ.

Detección automática de saturación: Su enfoque utiliza un umbral simple. Las herramientas profesionales determinan la saturación de forma más sofisticada mediante el análisis del histograma.

Rango dinámico cuantitativo: El Western blot de quimioluminiscencia tiene un rango dinámico estrecho; el intervalo de confianza cuantitativo es de apenas unos pocos factores. Las herramientas profesionales cargan una curva estándar junto con la imagen para verificar si cada banda se encuentra dentro de ese intervalo de confianza.

Western blot fluorescente: Los sistemas de fluorescencia, como LI-COR Odyssey, ofrecen un rango dinámico mucho más amplio y una cuantificación más precisa. El principio es el mismo, pero las características de la imagen difieren.


Proyectos de expansión

1. Visualización automática con matplotlib: Superponer y guardar las ROI de cada carril en la imagen para su posterior verificación.

2. Curva estándar: Cargar proteínas recombinantes de concentración conocida en algunos carriles y realizar un ajuste de curva estándar (curve fitting). Esto permite estimar la cantidad absoluta de las muestras desconocidas.

3. Aplicación Streamlit: Envolver su pipeline en una interfaz web. Carga de imagen → ajuste de ROI → descarga de resultados.

4. Procesamiento de múltiples anticuerpos: Datos de un mismo blot que ha sido sometido a stripping y reprobado con varios anticuerpos. Archivos separados por anticuerpo → emparejamiento → informe consolidado.


Mapa de componentes de este capítulo

  • [F] Matriz 2D de numpy: La imagen es una matriz. Extracción de ROI mediante slicing y cálculo de intensidad con sum().
  • [F] Correlación: Verificación de la correlación target-control con np.corrcoef. Validación de la necesidad de normalización.
  • [F] Manejo de excepciones: Aislamiento de archivos fallidos durante el procesamiento por lotes. Uso práctico de try-except.
  • [W] E/S de archivos · matplotlib: Carga de imágenes y visualización de resultados (script completo).

[F] = Implementado por usted / [W] = Código completo proporcionado.

💬 Preguntas y comentarios

0 comentarios

Puedes publicar sin iniciar sesión. Los comentarios de invitados no pueden editarse ni eliminarse después.

0/2000

Cargando...