Source code for xcal.phantom

import numpy as np
import cv2
from skimage.feature import canny
from scipy import ndimage as ndi

def _circle_mask(num_col, radius, xcenter=None, ycenter=None):
    """
    Generates a boolean mask for a circle given its radius and optional center coordinates.
    If the center is not specified, it defaults to the center of the array.

    Parameters:
    - num_col (int): Size of the square array to generate the mask for.
    - radius (float): Radius of the circle.
    - xcenter (float, optional): X-coordinate of the circle's center. Defaults to the center of the array.
    - ycenter (float, optional): Y-coordinate of the circle's center. Defaults to the center of the array.

    Returns:
    - numpy.ndarray: A boolean array where True values represent points inside the circle.
    """
    if xcenter == None:
        xcenter = (num_col - 1) / 2.0
    if ycenter == None:
        ycenter = (num_col - 1) / 2.0
    x = np.linspace(0, num_col - 1, num_col) - xcenter
    y = np.linspace(0, num_col - 1, num_col) - ycenter
    X, Y = np.meshgrid(x, y)
    mask = X ** 2 + Y ** 2 < radius ** 2
    return mask

[docs] def generate_circle_masks(side_length, pixel_size, num_circles, outer_cir_radius, inner_cir_radius): """ Generate a set of small, non-overlapping circular masks with their centers evenly distributed along the circumference of a larger circle, all contained within a square canvas. Args: side_length (float): The side length of the square canvas that the circles will be placed on, measured in millimeters (mm). This defines the workspace for the circle generation. pixel_size (float): The size of each pixel in the canvas, given in millimeters (mm), which determines the resolution of the created masks. num_circles (int): The number of non-overlapping small circles to generate. outer_cir_radius (float): The radius of the larger circle on whose circumference the centers of the small circles will be placed, measured in millimeters (mm). inner_cir_radius (float): The radius of each small inner circle mask, measured in millimeters (mm). The small circles will have their centers on the larger circle's circumference and should not overlap with each other. Returns: A list containing the circle masks, where each mask is represented by a boolean NumPy array with True values indicating the presence of the circle mask and False values indicating absence. """ # Initialize a list to store circle masks circle_masks = [] num_pixels = int(side_length / pixel_size) + 1 # Generate coordinates for non-overlapping circles theta_list = np.linspace(-np.pi, np.pi, num_circles, endpoint=False) for theta in theta_list: # Calculate the center coordinates of each circle center_x = side_length / 2 + outer_cir_radius * np.cos(theta) center_y = side_length / 2 + outer_cir_radius * np.sin(theta) # Generate a mask for the current circle # y, x = np.ogrid[:num_pixels, :num_pixels] # mask = (x * pixel_size - center_x) ** 2 + (y * pixel_size - center_y) ** 2 <= inner_cir_radius ** 2 mask = _circle_mask(num_pixels, inner_cir_radius/pixel_size, center_x/pixel_size, center_y/pixel_size) # Append the circle mask to the list circle_masks.append(mask) return circle_masks
[docs] def detect_hough_circles(phantom, radius_range=None, vmin=0, vmax=None, min_dist=100, HoughCircles_params1=300, HoughCircles_params2=1): """Detects circles in an image using the Hough Circle Transform. Args: phantom (numpy.ndarray): The 2D image to detect circles in. radius_range (list of int, optional): The minimum and maximum radius of circles to detect. Defaults to a range based on the image size. vmin (int, optional): Minimum value for clipping the image before detection. Defaults to 0. vmax (int, optional): Maximum value for clipping the image before detection. If None, the 90th percentile of the image is used. Defaults to None. min_dist (int, optional): Minimum distance between the centers of the detected circles. If too small, multiple neighbor circles may be falsely detected in addition to a true one. If too large, some circles may be missed. Defaults to 100. HoughCircles_params1 (float, optional): Upper threshold for the internal edge detection. HoughCircles_params2 (float, optional): Threshold for center detection, which influences the detection sensitivity. Large param2 leads to fewer detected circles. Returns: numpy.ndarray: An array of detected circles, each represented by the center coordinates (x, y) and radius. Returns an empty array if no circles are detected. """ # Set the default radius range based on the image size if not provided if radius_range is None: radius_range = [phantom.shape[0] // 50, 2 * phantom.shape[0] // 5] # Set the default maximum value for clipping if not provided if vmax is None: vmax = np.quantile(phantom, 0.9) # Clip and normalize the image tg_phan = np.clip(phantom, vmin, vmax) tg_img = np.uint8(255 * (tg_phan - vmin) / (vmax - vmin)) # Detect circles using the Hough Circle Transform detected_circles = cv2.HoughCircles(tg_img, cv2.HOUGH_GRADIENT, dp=1, minDist=min_dist, param1=HoughCircles_params1, param2=HoughCircles_params2, minRadius=radius_range[0], maxRadius=radius_range[1]) return detected_circles[0]
[docs] def segment_object(phantom, vmin, vmax, canny_sigma, roi_radius=None, bbox=None): """Segments an object within a given region of interest (ROI) or bounding box in an image. This function creates a segmentation mask for an object in an image. The image values are clipped and normalized based on provided minimum and maximum values. Canny edge detection is then applied to the normalized image. The edges are filled to create a binary mask that segments the object. Args: phantom (np.array): The input image to segment. vmin (float): The minimum value for clipping the image. vmax (float): The maximum value for clipping the image. canny_sigma (float): The standard deviation for the Gaussian filter used in Canny edge detection. roi_radius (int, optional): The radius of the circular region of interest. If not provided, it defaults to half the image size. bbox (tuple of int, optional): The bounding box within which to perform segmentation, specified as (r_min, c_min, r_max, c_max). If not provided, the entire image is used. Returns: np.array: A binary segmentation mask of the object. """ # Set default ROI radius to half the image size if not provided if roi_radius is None: roi_radius = phantom.shape[0] // 2 mask = _circle_mask(phantom.shape[0], roi_radius, xcenter=phantom.shape[0] // 2, ycenter=phantom.shape[0] // 2) # Apply bounding box if provided if bbox is not None: r_min, c_min, r_max, c_max = bbox mask_bbox = np.zeros(mask.shape, dtype=bool) mask_bbox[r_min:r_max, c_min:c_max] = True mask = mask & mask_bbox # Clip and normalize the phantom values tg = mask * np.clip(phantom - vmin, 0, vmax - vmin) / (vmax - vmin) * 255 # Perform Canny edge detection edges = canny(tg, sigma=canny_sigma) # Fill in the edges to create a mask filled_edges = ndi.binary_fill_holes(edges) # Convert boolean mask to an integer type segmentation_mask = filled_edges.astype(np.uint8) return segmentation_mask