import sys, os
sys.path.insert(0, os.path.abspath('.'))
import numpy as np
from skimage import feature
import torch
import matplotlib.pyplot as plt
import pydicom
from PIL import Image
import mammopy as mi
[docs]class analysis():
MASK_COLORS = ["red", "green", "blue", "yellow", "magenta", "cyan"]
[docs] def mask_to_rgba(mask, color="red"):
"""
Converts binary segmentation mask from white to red color.
Also adds alpha channel to make black background transparent.
Args:
mask (numpy.ndarray): [description]
color (str, optional): Check `MASK_COLORS` for available colors. Defaults to "red".
Returns:
numpy.ndarray: [description]
"""
assert color in analysis.MASK_COLORS
assert mask.ndim == 3 or mask.ndim == 2
h = mask.shape[0]
w = mask.shape[1]
zeros = np.zeros((h, w))
ones = mask.reshape(h, w)
if color == "red":
return np.stack((ones, zeros, zeros, ones), axis=-1)
elif color == "green":
return np.stack((zeros, ones, zeros, ones), axis=-1)
elif color == "blue":
return np.stack((zeros, zeros, ones, ones), axis=-1)
elif color == "yellow":
return np.stack((ones, ones, zeros, ones), axis=-1)
elif color == "magenta":
return np.stack((ones, zeros, ones, ones), axis=-1)
elif color == "cyan":
return np.stack((zeros, ones, ones, ones), axis=-1)
[docs] def density_estimation(model, img):
"""
Computes the percentage of mammogram density in an input image.
Args:
model (str): The name of the pre-trained model to use.
img: Input image in np.ndarray or PIL.Image format
Returns:
- image: The image in 256*256 size used for analysis
- pred1: The breast area of mammogram
- pred2: The dense area of mammogram
- percentage_density: The percentage of mammogram density in the input image.
Raises:
TypeError: If `model` is not a string or `image_path` is not a string.
ValueError: If `model` is not a valid pre-trained model name or `image_path`
does not point to a valid image file.
"""
if not isinstance(model, torch.nn.Module):
raise TypeError("Model must be a PyTorch model.")
if type(img) not in [np.ndarray, Image.Image]:
raise TypeError("Input must be np.ndarray or PIL.Image")
img = mi.pre_process.image_tensor(img)
# Compute the mammogram density
pred1, pred2 = model.module.predict(img)
img = img[0].cpu().numpy().transpose(1, 2, 0)
img = img[:, :, 0]
pred1 = pred1[0].cpu().numpy().transpose(1, 2, 0)
pred1 = pred1[:, :, 0]
pred2 = pred2[0].cpu().numpy().transpose(1, 2, 0)
pred2 = pred2[:, :, 0]
breast_area = np.sum(np.array(pred1) == 1)
dense_area = np.sum(np.array(pred2) == 1)
density = (dense_area / breast_area) * 100
density = np.rint(density)
return img, pred1, pred2, density
[docs] def single_density_estimation(model, img, visualization = False, breast_segmentation=False, dense_segmentation=False):
"""
Computes the percentage of mammogram density in an input image.
Args:
model (str): The name of the pre-trained model to use.
img: Input image in np.ndarray or PIL.Image format
visualization: For display output
breast_segmentation: Customize output to visualize Breast Area
dense_segmentation: Customize output to visualize Dense Area
Returns:
dict: A dictionary containing the results of the computation. The dictionary
has the following keys:
- non_dense_area: The total number of pixels in the input image that corresponse
to non-dense tissue. (i.e., non-fibroglandular)
- dense_area: The total number of pixels in the input image that corresponse
to dense tissue. (i.e., fibroglandular)
- percentage_density: The percentage of mammogram density in the input image.
Raises:
TypeError: If `model` is not a string or `image_path` is not a string.
ValueError: If `model` is not a valid pre-trained model name or `image_path`
does not point to a valid image file.
"""
if not isinstance(model, torch.nn.Module):
raise TypeError("Model must be a PyTorch model.")
if type(img) not in [np.ndarray, Image.Image]:
raise TypeError("Input must be np.ndarray or PIL.Image")
elif (visualization != True) and ((breast_segmentation == True) or (dense_segmentation == True)):
raise ValueError("For display option Visualization must be true")
# Compute the mammogram density
result = {}
img, pred1, pred2, _ = mi.analysis.density_estimation(model, img)
breast_area = np.sum(np.array(pred1) == 1)
dense_area = np.sum(np.array(pred2) == 1)
density = (dense_area / breast_area) * 100
density = np.rint(density)
# Populate the result dictionary with the computed values
result["Non_Dense_Area"] = breast_area
result["Dense_Area"] = dense_area
result["Percentage_Density"] = density
if visualization == False:
return result
elif (visualization == True) and (breast_segmentation == True) and (dense_segmentation == False):
edges = analysis.canny_edges(pred1)
plt.title('Breast area contour')
plt.imshow(img, cmap='gray')
plt.imshow(analysis.mask_to_rgba(edges, color='red'), cmap='gray')
plt.axis('off')
plt.show()
return result
elif (visualization == True) and (breast_segmentation == False) and (dense_segmentation == True):
plt.title('Dense tissues')
plt.imshow(img, cmap='gray')
plt.imshow(analysis.mask_to_rgba(pred2, color='green'), cmap='gray')
plt.axis('off')
plt.show()
return result
else:
edges = analysis.canny_edges(pred1)
fig, axes = plt.subplots(1,2, figsize = (15,10),squeeze=False)
axes[0, 0].set_title('Image', fontsize=16)
axes[0, 1].set_title("Breast and dense tissue segmentation", fontsize=20)
axes[0, 0].imshow(img, cmap='gray')
axes[0, 0].set_axis_off()
axes[0, 1].imshow(img, cmap='gray')
axes[0, 1].imshow(analysis.mask_to_rgba(edges, color='red'), cmap='gray')
axes[0, 1].imshow(analysis.mask_to_rgba(pred2, color='green'), cmap='gray', alpha=0.7)
axes[0, 1].set_axis_off()
plt.show()
return result
[docs] def canny_edges(image_array):
"""
Detect edges using the Canny algorithm.
Parameters:
image_array (numpy.ndarray): The input image array
Returns:
edges (numpy.ndarray): A binary edge map with detected edges marked with 1.
Raises:
TypeError: If the input image is not a numpy array.
"""
if not isinstance(image_array, np.ndarray):
raise TypeError("Input image must be a numpy array.")
# Detect edges using Canny algorithm with sigma value of 3.
edges = feature.canny(image_array, sigma=3)
return edges
[docs] def multi_density_estimation(model, folder_path):
"""
Computes the percentage of mammogram density in an input image.
Args:
model (str): The name of the pre-trained model to use.
folder_path (str): The path to the input folder of files.
Returns:
dict: A dictionary containing the results of the computation of each valid file. The dictionary
has the following keys:
- File: Name of the valid file, Skips the analysis for wrong file format
- Non_Dense_Area: The total number of pixels in the input file that corresponse
to non-dense tissue. (i.e., non-fibroglandular)
- Dense_Area: The total number of pixels in the input file that corresponse
to dense tissue. (i.e., fibroglandular)
- Percentage_Density: The percentage of mammogram density of the input files.
Raises:
TypeError: If `model` is not a string or `folder_path` is not a string.
ValueError: If `model` is not a valid pre-trained model name or `folder_path`
does not point to a valid folder.
"""
if not isinstance(model, torch.nn.Module):
raise TypeError("Model must be a PyTorch model.")
if os.path.exists(folder_path):
files = os.scandir(folder_path)
else:
raise FileExistsError("Folder path must be a string with '/' forward slashs")
result = []
for file in files:
if file.is_file():
file_ext = file.name.split(".")[-1]
if file_ext in ['jpg', 'JPG', 'jpeg', 'JPEG', 'png', 'PNG', 'tif', 'tiff', 'TIF', 'TIFF']:
print(file.name)
img = Image.open(open(file, 'rb')).convert('RGB')
temp = analysis.single_density_estimation(model=model, img=img)
elif file_ext == 'dcm' or file_ext == 'DCM':
dicom_file = pydicom.dcmread(file)
pil_image = Image.fromarray(dicom_file.pixel_array)
img = pil_image.convert('RGB')
temp = analysis.single_density_estimation(model=model, img=img)
else:
try:
ds = pydicom.dcmread(file)
pixel_array = ds.pixel_array
if ds.PhotometricInterpretation == 'MONOCHROME1':
pixel_array = (pixel_array * 255.0 / np.max(pixel_array)).astype(np.uint8)
pil_image = Image.fromarray(pixel_array)
img = pil_image.convert('RGB')
img = np.max(img) - img
np_to_PILimg = Image.fromarray(img)
temp = analysis.single_density_estimation(model=model, img=np_to_PILimg)
else:
temp = None
pass
except pydicom.errors.InvalidDicomError:
temp = None
pass
# Populate the result dictionary with the computed values
if temp is None:
pass
else:
temp = {'File': file.name, "Non_Dense_Area": temp["Non_Dense_Area"], 'Dense_Area': temp["Dense_Area"], 'Percentage_Density': temp["Percentage_Density"]}
result.append(temp)
return result