| import SimpleITK as sitk |
| import numpy as np |
| import pandas as pd |
| import os |
| import radiomics |
| from radiomics import featureextractor |
| import argparse |
| import numpy as np |
| from sklearn.cluster import KMeans |
| import scipy.ndimage as ndimage |
| from sklearn.mixture import GaussianMixture |
| import skfuzzy as fuzz |
| from skimage.filters import threshold_otsu |
| from skimage.filters import threshold_otsu |
| from skimage.segmentation import watershed |
| from skimage.feature import peak_local_max |
| from skimage import morphology |
| from scipy.ndimage import distance_transform_edt, binary_erosion |
| from matplotlib import colors |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import SimpleITK as sitk |
| import numpy as np |
| import pandas as pd |
| from matplotlib import colors |
| import cv2 |
| from skimage import measure |
|
|
|
|
| def make_bold(text): |
| return f"\033[1m{text}\033[0m" |
|
|
| def load_itk_image(filename): |
| itkimage = sitk.ReadImage(filename) |
| numpyImage = sitk.GetArrayFromImage(itkimage) |
| numpyOrigin = itkimage.GetOrigin() |
| numpySpacing = itkimage.GetSpacing() |
| return numpyImage, numpyOrigin, numpySpacing |
|
|
| def normalize_image_to_uint8(image, lower_bound=-1000, upper_bound=100): |
| clipped_img = np.clip(image, lower_bound, upper_bound) |
| normalized_img = ((clipped_img - lower_bound) / (upper_bound - lower_bound)) * 255.0 |
| normalized_img = normalized_img.astype(np.uint8) |
| return normalized_img |
|
|
|
|
|
|
|
|
| def segment_nodule_kmeans(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2): |
| """ |
| Segments a nodule in a 3D CT image using k-means clustering with a margin around the bounding box. |
| |
| Parameters: |
| - ct_image: 3D NumPy array representing the CT image. |
| - bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box. |
| - bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box. |
| - margin: Margin to add around the bounding box (default is 5). |
| - n_clusters: Number of clusters to use in k-means (default is 2). |
| |
| Returns: |
| - segmented_image: 3D NumPy array with the segmented nodule. |
| """ |
|
|
| x_center, y_center, z_center = bbox_center |
| w, h, d = bbox_whd |
|
|
| |
| x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin) |
| y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin) |
| z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin) |
|
|
| bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end] |
|
|
| |
| flat_region = bbox_region.reshape(-1, 1) |
|
|
| |
| kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(flat_region) |
| labels = kmeans.labels_ |
|
|
| |
| clustered_region = labels.reshape(bbox_region.shape) |
|
|
| |
| nodule_cluster = np.argmax(kmeans.cluster_centers_) |
|
|
| |
| nodule_mask = (clustered_region == nodule_cluster) |
|
|
| |
| nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3))) |
| nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((2, 2, 2))) |
|
|
| |
| segmented_image = np.zeros_like(ct_image, dtype=np.uint8) |
|
|
| |
| segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask |
|
|
| return segmented_image |
|
|
|
|
| def segment_nodule_gmm(ct_image, bbox_center, bbox_whd, margin=5, n_components=2): |
| """ |
| Segments a nodule in a 3D CT image using a Gaussian Mixture Model with a margin around the bounding box. |
| |
| Parameters: |
| - ct_image: 3D NumPy array representing the CT image. |
| - bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box. |
| - bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box. |
| - margin: Margin to add around the bounding box (default is 5). |
| - n_components: Number of components to use in the Gaussian Mixture Model (default is 2). |
| |
| Returns: |
| - segmented_image: 3D NumPy array with the segmented nodule. |
| """ |
|
|
| x_center, y_center, z_center = bbox_center |
| w, h, d = bbox_whd |
|
|
| |
| x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin) |
| y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin) |
| z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin) |
|
|
| bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end] |
|
|
| |
| flat_region = bbox_region.reshape(-1, 1) |
|
|
| |
| gmm = GaussianMixture(n_components=n_components, random_state=0).fit(flat_region) |
| labels = gmm.predict(flat_region) |
|
|
| |
| clustered_region = labels.reshape(bbox_region.shape) |
|
|
| |
| nodule_component = np.argmax(gmm.means_) |
|
|
| |
| nodule_mask = (clustered_region == nodule_component) |
|
|
| |
| nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3))) |
| nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3))) |
|
|
| |
| segmented_image = np.zeros_like(ct_image, dtype=np.uint8) |
|
|
| |
| segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask |
|
|
| return segmented_image |
|
|
|
|
| def segment_nodule_fcm(ct_image, bbox_center, bbox_whd, margin=5, n_clusters=2): |
| """ |
| Segments a nodule in a 3D CT image using Fuzzy C-means clustering with a margin around the bounding box. |
| |
| Parameters: |
| - ct_image: 3D NumPy array representing the CT image. |
| - bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box. |
| - bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box. |
| - margin: Margin to add around the bounding box (default is 5). |
| - n_clusters: Number of clusters to use in Fuzzy C-means (default is 2). |
| |
| Returns: |
| - segmented_image: 3D NumPy array with the segmented nodule. |
| """ |
|
|
| x_center, y_center, z_center = bbox_center |
| w, h, d = bbox_whd |
|
|
| |
| x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin) |
| y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin) |
| z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin) |
|
|
| bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end] |
|
|
| |
| flat_region = bbox_region.reshape(-1, 1) |
|
|
| |
| cntr, u, u0, d, jm, p, fpc = fuzz.cluster.cmeans(flat_region.T, n_clusters, 2, error=0.005, maxiter=1000, init=None) |
|
|
| |
| labels = np.argmax(u, axis=0) |
|
|
| |
| clustered_region = labels.reshape(bbox_region.shape) |
|
|
| |
| nodule_cluster = np.argmax(cntr) |
|
|
| |
| nodule_mask = (clustered_region == nodule_cluster) |
|
|
| |
| nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3))) |
| nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3))) |
|
|
| |
| segmented_image = np.zeros_like(ct_image, dtype=np.uint8) |
|
|
| |
| segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask |
|
|
| return segmented_image |
|
|
|
|
|
|
| def segment_nodule_otsu(ct_image, bbox_center, bbox_whd, margin=5): |
| """ |
| Segments a nodule in a 3D CT image using Otsu's thresholding with a margin around the bounding box. |
| |
| Parameters: |
| - ct_image: 3D NumPy array representing the CT image. |
| - bbox_center: Tuple of (x, y, z) coordinates for the center of the bounding box. |
| - bbox_whd: Tuple of (w, h, d) representing the width, height, and depth of the bounding box. |
| - margin: Margin to add around the bounding box (default is 5). |
| |
| Returns: |
| - segmented_image: 3D NumPy array with the segmented nodule. |
| """ |
|
|
| x_center, y_center, z_center = bbox_center |
| w, h, d = bbox_whd |
|
|
| |
| x_start, x_end = max(0, x_center - w//2 - margin), min(ct_image.shape[0], x_center + w//2 + margin) |
| y_start, y_end = max(0, y_center - h//2 - margin), min(ct_image.shape[1], y_center + h//2 + margin) |
| z_start, z_end = max(0, z_center - d//2 - margin), min(ct_image.shape[2], z_center + d//2 + margin) |
|
|
| bbox_region = ct_image[x_start:x_end, y_start:y_end, z_start:z_end] |
|
|
| |
| flat_region = bbox_region.flatten() |
|
|
| |
| otsu_threshold = threshold_otsu(flat_region) |
|
|
| |
| nodule_mask = bbox_region >= otsu_threshold |
|
|
| |
| nodule_mask = ndimage.binary_closing(nodule_mask, structure=np.ones((3, 3, 3))) |
| nodule_mask = ndimage.binary_opening(nodule_mask, structure=np.ones((3, 3, 3))) |
|
|
| |
| segmented_image = np.zeros_like(ct_image, dtype=np.uint8) |
|
|
| |
| segmented_image[x_start:x_end, y_start:y_end, z_start:z_end] = nodule_mask |
|
|
| return segmented_image |
|
|
|
|
|
|
| def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm): |
| """ |
| Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values. |
| |
| Parameters: |
| segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background). |
| spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z). |
| expansion_mm (float): Distance to expand the mask in millimeters. |
| |
| Returns: |
| numpy array: Expanded segmentation mask. |
| """ |
| |
| spacing_reordered = (spacing[2], spacing[1], spacing[0]) |
|
|
| |
| expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered]) |
|
|
| |
| expanded_mask = np.zeros_like(segmented_nodule_gmm) |
|
|
| |
| white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1) |
|
|
| |
| for coord in white_pixel_coords: |
| z, y, x = coord |
|
|
| |
| z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1)) |
| y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1)) |
| x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1)) |
|
|
| |
| for z_new in z_range: |
| for y_new in y_range: |
| for x_new in x_range: |
| expanded_mask[z_new, y_new, x_new] = 1 |
|
|
| return expanded_mask |
|
|
|
|
| def find_nodule_lobe(cccwhd, lung_mask, class_map): |
| """ |
| Determine the lung lobe where a nodule is located based on a 3D mask and bounding box. |
| |
| Parameters: |
| cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth]. |
| lung_mask (numpy array): 3D array representing the lung mask with different lung regions. |
| class_map (dict): Dictionary mapping lung region labels to their names. |
| |
| Returns: |
| str: Name of the lung lobe where the nodule is located. |
| """ |
| center_x, center_y, center_z, width, height, depth = cccwhd |
|
|
| |
| start_x = int(center_x - width // 2) |
| end_x = int(center_x + width // 2) |
| start_y = int(center_y - height // 2) |
| end_y = int(center_y + height // 2) |
| start_z = int(center_z - depth // 2) |
| end_z = int(center_z + depth // 2) |
|
|
| |
| start_x = max(0, start_x) |
| end_x = min(lung_mask.shape[0], end_x) |
| start_y = max(0, start_y) |
| end_y = min(lung_mask.shape[1], end_y) |
| start_z = max(0, start_z) |
| end_z = min(lung_mask.shape[2], end_z) |
|
|
| |
| roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z] |
|
|
| |
| unique, counts = np.unique(roi, return_counts=True) |
| label_counts = dict(zip(unique, counts)) |
|
|
| |
| if 0 in label_counts: |
| del label_counts[0] |
|
|
| |
| if label_counts: |
| nodule_lobe = max(label_counts, key=label_counts.get) |
| else: |
| nodule_lobe = None |
|
|
| |
| if nodule_lobe is not None: |
| nodule_lobe_name = class_map["lungs"][nodule_lobe] |
| else: |
| nodule_lobe_name = "Undefined" |
|
|
| return nodule_lobe_name |
|
|
|
|
| def find_nodule_lobe_and_distance(cccwhd, lung_mask, class_map,spacing): |
| """ |
| Determine the lung lobe where a nodule is located and measure its distance from the lung wall. |
| |
| Parameters: |
| cccwhd (list or tuple): Bounding box in the format [center_x, center_y, center_z, width, height, depth]. |
| lung_mask (numpy array): 3D array representing the lung mask with different lung regions. |
| class_map (dict): Dictionary mapping lung region labels to their names. |
| |
| Returns: |
| tuple: (Name of the lung lobe, Distance from the lung wall) |
| """ |
| center_x, center_y, center_z, width, height, depth = cccwhd |
|
|
| |
| start_x = int(center_x - width // 2) |
| end_x = int(center_x + width // 2) |
| start_y = int(center_y - height // 2) |
| end_y = int(center_y + height // 2) |
| start_z = int(center_z - depth // 2) |
| end_z = int(center_z + depth // 2) |
|
|
| |
| start_x = max(0, start_x) |
| end_x = min(lung_mask.shape[0], end_x) |
| start_y = max(0, start_y) |
| end_y = min(lung_mask.shape[1], end_y) |
| start_z = max(0, start_z) |
| end_z = min(lung_mask.shape[2], end_z) |
|
|
| |
| roi = lung_mask[start_x:end_x, start_y:end_y, start_z:end_z] |
|
|
| |
| unique, counts = np.unique(roi, return_counts=True) |
| label_counts = dict(zip(unique, counts)) |
|
|
| |
| if 0 in label_counts: |
| del label_counts[0] |
|
|
| |
| if label_counts: |
| nodule_lobe = max(label_counts, key=label_counts.get) |
| else: |
| nodule_lobe = None |
|
|
| |
| if nodule_lobe is not None: |
| nodule_lobe_name = class_map["lungs"][nodule_lobe] |
| else: |
| nodule_lobe_name = "Undefined" |
|
|
| |
| nodule_centroid = np.array([center_x, center_y, center_z]) |
| |
| |
| lung_binary_mask = lung_mask > 0 |
|
|
| |
| |
| lung_eroded = binary_erosion(lung_binary_mask) |
| lung_wall_mask = lung_binary_mask & ~lung_eroded |
|
|
| |
| distance_transform = distance_transform_edt(~lung_wall_mask) |
|
|
| |
| |
| |
| voxel_distance_to_lung_wall = distance_transform[center_x, center_y, center_z] |
|
|
| |
| physical_distance_to_lung_wall = voxel_distance_to_lung_wall * np.sqrt( |
| spacing[0]**2 + spacing[1]**2 + spacing[2]**2 |
| ) |
|
|
|
|
|
|
| return nodule_lobe_name, voxel_distance_to_lung_wall,physical_distance_to_lung_wall |
|
|
|
|
| |
| def expand_mask_by_distance(segmented_nodule_gmm, spacing, expansion_mm): |
| """ |
| Expands the segmentation mask by a given distance in mm in all directions by directly updating pixel values. |
| |
| Parameters: |
| segmented_nodule_gmm (numpy array): 3D binary mask of the nodule (1 for nodule, 0 for background). |
| spacing (tuple): Spacing of the image in mm for each voxel, given as (spacing_x, spacing_y, spacing_z). |
| expansion_mm (float): Distance to expand the mask in millimeters. |
| |
| Returns: |
| numpy array: Expanded segmentation mask. |
| """ |
| |
| spacing_reordered = (spacing[2], spacing[1], spacing[0]) |
|
|
| |
| expand_pixels = np.array([int(np.round(expansion_mm / s)) for s in spacing_reordered]) |
|
|
| |
| expanded_mask = np.zeros_like(segmented_nodule_gmm) |
|
|
| |
| white_pixel_coords = np.argwhere(segmented_nodule_gmm == 1) |
|
|
| |
| for coord in white_pixel_coords: |
| z, y, x = coord |
|
|
| |
| z_range = range(max(0, z - expand_pixels[0]), min(segmented_nodule_gmm.shape[0], z + expand_pixels[0] + 1)) |
| y_range = range(max(0, y - expand_pixels[1]), min(segmented_nodule_gmm.shape[1], y + expand_pixels[1] + 1)) |
| x_range = range(max(0, x - expand_pixels[2]), min(segmented_nodule_gmm.shape[2], x + expand_pixels[2] + 1)) |
|
|
| |
| for z_new in z_range: |
| for y_new in y_range: |
| for x_new in x_range: |
| expanded_mask[z_new, y_new, x_new] = 1 |
|
|
| return expanded_mask |
|
|
|
|
|
|
| |
| def plot_contours(ax, mask, color, linewidth=1.5): |
| contours = measure.find_contours(mask, level=0.5) |
| for contour in contours: |
| ax.plot(contour[:, 1], contour[:, 0], color=color, linewidth=linewidth) |
|
|
|
|