En este artículo veremos cómo realizar la segmentación de tumores cerebrales a partir de imágenes de resonancia magnética. Probaremos diferentes arquitecturas que son populares para problemas de segmentación de imágenes.
Empecemos por definir cuál es nuestro problema empresarial.
Problema comercial:
Un tumor cerebral es una masa anormal de tejido en la que las células crecen y se multiplican abruptamente, que permanece sin control por los mecanismos que controlan las células normales.
Los tumores cerebrales se clasifican en tumores benignos o de bajo grado (grado I o II) y malignos o de alto grado (grado III y IV).Los tumores benignos no son cancerosos y se consideran no progresivos, su crecimiento es relativamente lento y limitado.
Sin embargo, los tumores malignos son cancerosos y crecen rápidamente con límites indefinidos. Por lo tanto, la detección temprana de tumores cerebrales es crucial para el tratamiento adecuado y para salvar vidas humanas.
Planteamiento del problema:
El problema que estamos tratando de resolver es la segmentación de imágenes. La segmentación de imágenes es el proceso de asignar una etiqueta de clase (como persona, automóvil o árbol) a cada píxel de una imagen.
Puede pensar en ello como una clasificación, pero a nivel de píxel, en lugar de clasificar la imagen completa bajo una etiqueta, clasificaremos cada píxel por separado.
Conjunto de datos:
El conjunto de datos se descarga de Kaggle.
Este conjunto de datos contiene imágenes de resonancia magnética del cerebro junto con máscaras manuales de segmentación de anomalías FLAIR.
Las imágenes se obtuvieron de The Cancer Imaging Archive (TCIA). Corresponden a 110 pacientes incluidos en la colección de gliomas de grado inferior de The Cancer Genome Atlas (TCGA) con al menos secuencia de recuperación de inversión atenuada por líquido (FLAIR) y datos de grupos genómicos disponibles.
Los grupos genómicos tumorales y los datos del paciente se proporcionan en el archivo data.csv. Las imágenes están en formato tif.
Análisis exploratorio y preprocesamiento:
La siguiente es una imagen de muestra y su máscara correspondiente de nuestro conjunto de datos.
Imprimamos una imagen del cerebro que tiene un tumor junto con su máscara.
Ahora vamos a comprobar la distribución de imágenes tumorales y no tumorales en el conjunto de datos.
Aquí 1 indica tumor y 0 indica ausencia de tumor. Disponemos de un total de 2556 imágenes no tumorales y 1373 tumorales.
Como paso previo al procesamiento, recortaremos la parte de la imagen que contiene solo el cerebro. Antes de recortar la imagen, tenemos que lidiar con un problema importante que es el bajo contraste.
Un problema común con las imágenes de resonancia magnética es que a menudo sufren de bajo contraste. Por lo tanto, mejorar el contraste de la imagen mejorará en gran medida el rendimiento de los modelos.
Por ejemplo, eche un vistazo a la siguiente imagen de nuestro conjunto de datos.
A simple vista no podemos ver nada. Es completamente negro. Intentemos mejorar el contraste de esta imagen.
Hay dos formas comunes de mejorar el contraste.
- Ecualización de histogramas y
- Ecualización de histograma adaptativo limitado de contraste (CLAHE)
Primero intentaremos la ecualización de histogramas. Podemos usar OpenCV igualarHist()
#dado que esta es una imagen en color, tenemos que aplicar #la ecualización del histograma en cada uno de los tres canales por separado #cv2.split devolverá los tres canales en el orden B, G, R b,g,r = cv2.split(img ) #aplicar hist equ en los tres canales por separado b = cv2.equalizeHist(b) g = cv2.equalizeHist(g) r = cv2.equalizeHist(r) #fusionar los tres canales equ = cv2.merge((b,g ,r)) #convertirlo a RGB para visualizar equ = cv2.cvtColor(equ,cv2.COLOR_BGR2RGB); plt.imshow(equivalente)
#dado que esta es una imagen en color, tenemos que aplicar #la ecualización del histograma en cada uno de los tres canales por separado #cv2.split devolverá los tres canales en el orden B, G, R b,gramo,r = cv2.separar(imagen) #aplicar hist equ en los tres canales por separado b = cv2.igualarHist(b) gramo = cv2.igualarHist(gramo) r = cv2.igualarHist(r) #merge los tres canales equ = cv2.unir((b,gramo,r)) #convertirlo a RGB para visualizar equ = cv2.cvtColor(equ,cv2.COLOR_BGR2RGB); por favor.immostrar(equ) |
La siguiente es la imagen ecualizada del histograma.
Ahora vamos a aplicar CLAHE. Usaremos OpenCV crearCLAHE()
#haga lo mismo que hicimos para la ecualización del histograma #establezca el valor del clip y el tamaño de la cuadrícula cambiando estos valores dará una salida diferente clahe = cv2.createCLAHE(clipLimit=6, tileGridSize=(16,16)) #divida los tres canales b ,g,r = cv2.split(img) #aplicar CLAHE en los tres canales por separado b = clahe.apply(b) g = clahe.apply(g) r = clahe.apply(r) #fusionar los tres canales bgr = cv2.merge((b,g,r)) #convertir a RGB y trazar cl = cv2.cvtColor(bgr,cv2.COLOR_BGR2RGB); cv2_imshow(cl)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dieciséis 17 18 19 |
#haz lo mismo que hicimos para la ecualización del histograma #establecer el valor del clip y el tamaño de la cuadrícula cambiando estos valores dará una salida diferente claro = cv2.crearCLAHE(límite de clip=6, TileGridSize=(dieciséis,dieciséis)) #dividir los tres canales b,gramo,r = cv2.separar(imagen) #aplicar CLAHE en los tres canales por separado b = claro.solicitar(b) gramo = claro.solicitar(gramo) r = claro.solicitar(r) #fusionar los tres canales bgr = cv2.unir((b,gramo,r)) #convertir a RGB y trazar cl = cv2.cvtColor(bgr,cv2.COLOR_BGR2RGB); cv2_imshow(cl) |
La siguiente es la imagen después de aplicar CLAHE
De los resultados tanto de la ecualización del histograma como de CLAHE, podemos concluir que CLAHE produce mejores resultados. La imagen que obtuvimos del ecualizador de histograma parece poco natural en comparación con CLAHE.
Ahora podemos proceder a recortar la imagen.
El siguiente es el procedimiento que seguiremos para recortar una imagen.
1) Primero cargaremos la imagen.
2) Luego aplicaremos CLAHE para potenciar el contraste de la imagen.
3) Una vez que se mejore el contraste, detectaremos los bordes en la imagen.
4) Luego aplicaremos la operación de dilatación para eliminar pequeñas regiones de ruidos.
5) Ahora podemos encontrar los contornos en la imagen. Una vez que tengamos los contornos buscaremos los puntos extremos del contorno y recortaremos la imagen.

La imagen de arriba muestra el proceso de mejora del contraste y recorte de una sola imagen. De manera similar, haremos esto para todas las imágenes en el conjunto de datos.
El siguiente código realizará el paso de procesamiento previo y guardará las imágenes recortadas y sus máscaras.
import cv2 from google.colab.patches import cv2_imshow from tqdm import tqdm def crop_img(): #recorre todas las imágenes y su correspondiente máscara para i en tqdm(range(len(df))): image = cv2.imread(df[‘train’].iloc[i]) máscara = cv2.imread(df[‘mask’].iloc[i]) clahe = cv2.createCLAHE(clipLimit=4, tileGridSize=(16,16)) b,g,r = cv2.split(image) #aplicar CLAHE en los tres canales por separado b = clahe.apply(b) g = clahe.apply(g) r = clahe.apply(r) #combinar los tres canales bgr = cv2.merge((b,g,r)) #convertir a RGB cl = cv2.cvtColor(bgr,cv2.COLOR_BGR2RGB) imgc = cl.copy() #detectar bordes edged = cv2.Canny(cl, 10, 250) #realizar dilatación edged = cv2.dilate(edged, None, iterations=2) #finding_contours (cnts, _) = cv2.findContours( closed.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(cnts) == 0: #Si no hay contornos guardar la imagen mejorada CLAHE cv2.imwrite(‘/content/train/’+df[‘train’].iloc[i][-28:]imgc) cv2.imwrite(‘/contenido/máscara/’+df[‘mask’].iloc[i][-33:]máscara) else: #encuentre los puntos extremos en el contorno y recorte la imagen #https://www.pyimagesearch.com/2016/04/11/finding-extreme-points-in-contours-with-opencv/ c = max(cnts, clave=cv2.contourArea) extLeft = tupla(c[c[:, :, 0].argmin()][0]) extDerecha = tupla(c[c[:, :, 0].argmax()][0]) extSuperior = tupla(c[c[:, :, 1].argmin()][0]) extBot = tupla(c[c[:, :, 1].argmax()][0]) nueva_imagen = imgc[extTop[1]:extBot[1]extLeft[0]:extDerecha[0]]máscara = máscara[extTop[1]:extBot[1]extLeft[0]:extDerecha[0]]#guardar la imagen y su máscara correspondiente cv2.imwrite(‘/content/train/’+df[‘train’].iloc[i][-28:]nueva_imagen) cv2.imwrite(‘/contenido/máscara/’+df[‘mask’].iloc[i][-33:]máscara) crop_img()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dieciséis 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
importar cv2 de Google.colaboración.parches importar cv2_imshow de tqdm importar tqdm definitivamente crop_img(): #bucle por todas las imágenes y su máscara correspondiente por i en tqdm(alcance(Len(d.f.))): imagen = cv2.Estoy leído(d.f.[‘train’].iloc[i])
máscara = cv2.Estoy leído(d.f.[‘mask’].iloc[i])
claro = cv2.crearCLAHE(límite de clip=4, TileGridSize=(dieciséis,dieciséis)) b,gramo,r = cv2.separar(imagen) #aplicar CLAHE en los tres canales por separado b = claro.solicitar(b) gramo = claro.solicitar(gramo) r = claro.solicitar(r) #fusionar los tres canales bgr = cv2.unir((b,gramo,r)) #convertir a RGB cl = cv2.cvtColor(bgr,cv2.COLOR_BGR2RGB)
imgc = cl.Copiar()
#detectar bordes afilado = cv2.Astuto(cl, 10, 250) #realizar dilatación afilado = cv2.dilatar(afilado, Ninguno, iteraciones=2)
#buscando_contornos
(centavos, _) = cv2.encontrarContornos(cerrado.Copiar(), cv2.RETR_EXTERNO, cv2.CADENA_APPROX_SIMPLE)
si Len(centavos) == 0: #Si no hay contornos guarde la imagen mejorada de CLAHE cv2.soy escritura(‘/contenido/tren/’+d.f.[‘train’].iloc[i][–28:], imgc) cv2.soy escritura(‘/contenido/máscara/’+d.f.[‘mask’].iloc[i][–33:], máscara) más: #encuentra los puntos extremos en el contorno y recorta los imagen #https://www.pyimagesearch.com/2016/04/11/encontrar-puntos-extremos-en-contornos-con-opencv/ C = máximo(centavos, llave=cv2.área de contorno) extLeft = tupla(C[c[:, :, 0].argmín()][0]) extDerecha = tupla(C[c[:, :, 0].argmax()][0]) extTop = tupla(C[c[:, :, 1].argmín()][0]) extBot = tupla(C[c[:, :, 1].argmax()][0]) nueva imagen = imgc[extTop[1]:extBot[1], extLeft[0]:extDerecha[0]] máscara = máscara[extTop[1]:extBot[1], extLeft[0]:extDerecha[0]]
#guardar la imagen y su máscara correspondiente cv2.soy escritura(‘/contenido/tren/’+d.f.[‘train’].iloc[i][–28:], nueva imagen) cv2.soy escritura(‘/contenido/máscara/’+d.f.[‘mask’].iloc[i][–33:], máscara) crop_img() |
Métricas de evaluación:
Antes de continuar con la parte de modelado, debemos definir nuestras métricas de evaluación.
Las métricas más populares para los problemas de segmentación de imágenes son el coeficiente de dados y la intersección sobre unión (IOU).
pagaré:
IoU es el área de superposición entre la segmentación predicha y la verdad básica dividida por el área de unión entre la segmentación predicha y la verdad básica.
pagaré = frac
Coeficiente de dados:
El coeficiente de dados es 2 * el área de superposición dividida por el número total de píxeles en ambas imágenes.
Coeficiente de dados = frac
1 – El coeficiente de dados nos dará la pérdida de dados. Por el contrario, la gente también calcula la pérdida de dados como -(coeficiente de dados). Podemos elegir cualquiera de los dos.
Sin embargo, el rango de pérdida de dados difiere según cómo lo calculemos. Si calculamos la pérdida de dados como 1-dice_coeff entonces el rango será [0,1] y si calculamos la pérdida como -(dice_coeff) entonces el rango será [-1, 0].
Si desea obtener más información sobre el pagaré y el coeficiente de dados, puede leer esto excelente articulo por Ekin Tiu.
Modelos:
He entrenado totalmente a tres modelos. Son
- Red totalmente convolucional (FCN32)
- U-NET y
- red de resUstos
Los resultados de los modelos se encuentran a continuación.
Como puede ver en los resultados anteriores, el modelo ResUNet funciona mejor en comparación con otros modelos.
Sin embargo, si observa los valores del pagaré, está cerca de 1, lo que es casi perfecto. Esto podría deberse a que el área no tumoral es grande en comparación con la tumoral.
Entonces, para confirmar que el IOU de prueba alto no se debe a eso, calculemos los valores de IOU para las imágenes tumorales y no tumorales por separado.
Primero dividiremos nuestros datos de prueba en dos conjuntos de datos separados. Uno con imágenes tumorales y otro con imágenes no tumorales.
Una vez que hayamos dividido el conjunto de datos, podemos cargar nuestro modelo ResUnet y hacer las predicciones y obtener las puntuaciones de los dos conjuntos de datos por separado.
Los siguientes son los resultados por separado en las imágenes tumorales y no tumorales.
Los números se ven bien. Por lo tanto, podemos concluir que la puntuación no es alta debido al sesgo hacia las imágenes no tumorales que tienen un área relativamente grande en comparación con las imágenes tumorales.
Los siguientes son los resultados de muestra del modelo ResUNet.
Los resultados se ven bien. La imagen de la izquierda es la imagen de entrada. La del medio es la verdad básica y la imagen que está a la derecha es la predicción de nuestro modelo (ResUNet).
Para obtener el código completo de este artículo, visite este Repositorio de Github.
Referencias:
1) https://www.pyimagesearch.com/
2) https://opencv-python-tutroals.readthedocs.io/en/latest/index.html
3) https://www.kaggle.com/bonhart/brain-mri-data-visualization-unet-fpn
4) https://www.kaggle.com/monkira/brain-mri-segmentation-using-unet-keras