CoolFace
Apppublic

JFoz/test_path_analysis

sourceHugging Facebsd-3-clauseupdated 3y agoView on Hugging Face
0likes
analyse.py417 linesDownload Raw Back to path_analysis
1 2import lxml.etree as ET3import gzip4import tifffile5import matplotlib.pyplot as plt6import numpy as np7from PIL import Image, ImageDraw8import pandas as pd9from itertools import cycle10from .data_preprocess import analyse_traces11import math12import scipy.linalg as la13 14 15def get_paths_from_traces_file(traces_file):16    """17    Parses the specified traces file and extracts paths and their lengths.18    19    Args:20        traces_file (str): Path to the XML traces file.21 22    Returns:23        tuple: A tuple containing a list of paths (each path is a list of tuples representing points)24               and a list of corresponding path lengths.25    """26    tree = ET.parse(traces_file)27    root = tree.getroot()28    all_paths = []29    path_lengths = []30    for path in root.findall('path'):31        length=path.get('reallength')32        path_points = []33        for point in path:34            path_points.append((int(point.get('x')), int(point.get('y')), int(point.get('z'))))35        all_paths.append(path_points)36        path_lengths.append(float(length))37    return all_paths, path_lengths38 39 40 41def calculate_path_length_partials(point_list, voxel_size=(1,1,1)):42    """43    Calculate the partial path length of a series of points.44    45    Args:46    point_list (list of tuple): List of points, each represented as a tuple of coordinates (x, y, z).47    voxel_size (tuple, optional): Size of the voxel in each dimension (x, y, z). Defaults to (1, 1, 1).48    49    Returns:50    numpy.ndarray: Array of cumulative partial path lengths at each point.51    """52    # Simple calculation53    section_lengths = [0.0]54    s = np.array(voxel_size)55    for i in range(len(point_list)-1):56        # Euclidean distance between successive points57        section_lengths.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))58    return np.cumsum(section_lengths)59 60 61def visualise_ordering(points_list, dim, wr=5, wc=5):62    """63    Visualize the ordering of points in an image.64    65    Args:66        points_list (list): List of points to be visualized.67        dim (tuple): Dimensions of the image (rows, columns, channels).68        wr (int, optional): Width of the region to visualize around the point in the row direction. Defaults to 5.69        wc (int, optional): Width of the region to visualize around the point in the column direction. Defaults to 5.70 71    Returns:72        np.array: An image array with visualized points.73    """74    # Visualizes the ordering of the points in the list on a blank image.75    rdim, cdim, _ = dim76    vis = np.zeros((rdim, cdim, 3), dtype=np.uint8)77 78    def get_col(i):79        r = int(255 * i/len(points_list))80        g = 255 - r81        return r, g, 082 83    for n, p in enumerate(points_list):84        c, r, _ = map(int, p)85        vis[max(0,r-wr):min(rdim,r+wr+1),max(0,c-wc):min(cdim,c+wc+1)] = get_col(n)86 87    return vis88    89# A color map for paths90col_map = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255),91           (255,127,0), (255, 0, 127), (127, 255, 0), (0, 255, 127), (127,0,255), (0,127,255)]92 93def draw_paths(all_paths, foci_stack, foci_index=None, r=3, screened_foci_data=None):94    """95    Draws paths on the provided image stack and overlays markers for the foci96    97    Args:98        all_paths (list): List of paths where each path is a list of points.99        foci_stack (np.array): 3D numpy array representing the image stack.100        foci_index (list, optional): List of list of focus indices (along each path). Defaults to None.101        r (int, optional): Radius for the ellipse or line drawing around the focus. Defaults to 3.102        screened_foci_data (list, optional): List of RemovedPeakData for screened foci103    Returns:104        PIL.Image.Image: An image with the drawn paths.105    """106    im = np.max(foci_stack, axis=0)107    im = (im/np.max(im)*255).astype(np.uint8)108    im = np.dstack((im,)*3)109    im = Image.fromarray(im) 110    draw = ImageDraw.Draw(im)111    for i, (p, col) in enumerate(zip(all_paths, cycle(col_map))):112        draw.line([(u[0], u[1]) for u in p], fill=col)113        draw.text((p[0][0], p[0][1]), str(i+1), fill=col)114 115    if screened_foci_data is not None:116        for i, removed_peaks in enumerate(screened_foci_data):117            for p in removed_peaks:118                u = all_paths[i][p.idx]119                v = all_paths[p.screening_peak[0]][p.screening_peak[1]]120                draw.line((int(u[0]), int(u[1]), int(v[0]), int(v[1])), fill=(127,127,127), width=2)121 122    if foci_index is not None:123        for i, (idx, p, col) in enumerate(zip(foci_index, all_paths, cycle(col_map))):124            if len(idx):125                for j in idx:126                    draw.line((int(p[j][0]-r), int(p[j][1]), int(p[j][0]+r), int(p[j][1])), fill=col, width=2)127                    draw.line((int(p[j][0]), int(p[j][1]-r), int(p[j][0]), int(p[j][1]+r)), fill=col, width=2)128    return im129    130 131def measure_from_mask(mask, measure_stack):132    """133    Compute the sum of measure_stack values where the mask is equal to 1.134    135    Args:136        mask (numpy.ndarray): Binary mask where the measurement should be applied.137        measure_stack (numpy.ndarray): Stack of measurements.138        139    Returns:140        measure_stack.dtype: Sum of measure_stack values where the mask is 1.141    """142    return np.sum(mask * measure_stack)143 144# Max of measure_stack over region where mask==1145def max_from_mask(mask, measure_stack):146    """147    Compute the maximum of measure_stack values where the mask is equal to 1.148    149    Args:150        mask (numpy.ndarray): Binary mask where the measurement should be applied.151        measure_stack (numpy.ndarray): Stack of measurements.152        153    Returns:154        measure_stack.dtype: Maximum value of measure_stack where the mask is 1.155    """156    return np.max(mask * measure_stack)157 158def make_mask_s(p, melem, measure_stack):159    """160    Translate a mask to point p, ensuring correct treatment near the edges of the measure_stack.161    162    Args:163        p (tuple): Target point (r, c, z).164        melem (numpy.ndarray): Structuring element for the mask.165        measure_stack (numpy.ndarray): Stack of measurements.166        167    Returns:168        tuple: A tuple containing the translated mask and a section of the measure_stack.169    """170    171    172    # 173    174    R = [u//2 for u in melem.shape]175 176    r, c, z = p177 178    mask = np.zeros(melem.shape)179 180    m_data = np.zeros(melem.shape)181    s = measure_stack.shape182    o_1, o_2, o_3 = max(R[0]-r, 0), max(R[1]-c, 0), max(R[2]-z,0)183    e_1, e_2, e_3 = min(R[0]-r+s[0], 2*R[0]+1), min(R[1]-c+s[1], 2*R[1]+1), min(R[2]-z+s[2], 2*R[2]+1)184    m_data[o_1:e_1,o_2:e_2,o_3:e_3] = measure_stack[max(r-R[0],0):min(r+R[0]+1,s[0]),max(c-R[1],0):min(c+R[1]+1,s[1]),max(z-R[2],0):min(z+R[2]+1, s[2])]185    mask[o_1:e_1,o_2:e_2,o_3:e_3] = melem[o_1:e_1,o_2:e_2,o_3:e_3] 186 187    188    return mask, m_data189 190 191def measure_at_point(p, melem, measure_stack, op='mean'):192    """193    Measure the mean or max value of measure_stack around a specific point using a structuring element.194    195    Args:196        p (tuple): Target point (r, c, z).197        melem (numpy.ndarray): Structuring element for the mask.198        measure_stack (numpy.ndarray): Stack of measurements.199        op (str, optional): Operation to be applied; either 'mean' or 'max'. Default is 'mean'.200        201    Returns:202        float: Measured value based on the specified operation.203    """204 205    p = map(int, p)206    if op=='mean':207        mask, m_data = make_mask_s(p, melem, measure_stack)208        melem_size = np.sum(mask)209        return float(measure_from_mask(mask, m_data) / melem_size)210    else:211        mask, m_data = make_mask_s(p, melem, measure_stack)212        return float(max_from_mask(mask, m_data))213 214# Generate spherical region215def make_sphere(R=5, z_scale_ratio=2.3):216    """217    Generate a binary representation of a sphere in 3D space.218    219    Args:220        R (int, optional): Radius of the sphere. Default is 5. Centred on the centre of the middle voxel.221                           Includes all voxels whose centre is precisely R from the middle voxel.222        z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.223        224    Returns:225        numpy.ndarray: Binary representation of the sphere.226    """227    R_z = int(math.ceil(R/z_scale_ratio))228    x, y, z = np.ogrid[-R:R+1, -R:R+1, -R_z:R_z+1]229    sphere = x**2 + y**2 + (z_scale_ratio * z)**2 <= R**2230    return sphere231 232# Measure the values of measure_stack at each of the points of points_list in turn.233# Measurement is the mean / max (specified by op) on the spherical region about each point234def measure_all_with_sphere(points_list, measure_stack, op='mean', R=5, z_scale_ratio=2.3):235    """236    Measure the values of measure_stack at each point in a list using a spherical region.237    238    Args:239        points_list (list): List of points (r, c, z) to be measured.240        measure_stack (numpy.ndarray): Stack of measurements.241        op (str, optional): Operation to be applied; either 'mean' or 'max'. Default is 'mean'.242        R (int, optional): Radius of the sphere. Default is 5.243        z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.244        245    Returns:246        list: List of measured values for each point.247    """248    melem = make_sphere(R, z_scale_ratio)249    measure_func = lambda p: measure_at_point(p, melem, measure_stack, op)250    return list(map(measure_func, points_list))251 252 253# Measure fluorescence levels along ordered skeleton254def measure_chrom2(path, intensity, config):255    """256    Measure fluorescence levels along an ordered skeleton.257    258    Args:259        path (list): List of ordered path points (r, c, z).260        intensity (numpy.ndarray): 3D fluorescence data.261        config (dict): Configuration dictionary containing 'z_res', 'xy_res', and 'sphere_radius' values.262        263    Returns:264        tuple: A tuple containing the visualization, mean measurements, and max measurements along the path.265    """266    # Calculate size of spheroid used for measurement267    scale_ratio = config['z_res']/config['xy_res']268    sphere_xy_radius = int(math.ceil(config['sphere_radius']/config['xy_res']))269    270    vis = visualise_ordering(path, dim=intensity.shape, wr=sphere_xy_radius, wc=sphere_xy_radius)271 272    measurements = measure_all_with_sphere(path, intensity, op='mean', R=sphere_xy_radius, z_scale_ratio=scale_ratio)273    measurements_max = measure_all_with_sphere(path, intensity, op='max', R=sphere_xy_radius, z_scale_ratio=scale_ratio)274 275    276    return vis, measurements, measurements_max277 278def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):279    """280    Extract peak information from given traces and compile them into a DataFrame.281    282    Args:283    - cell_id (int or str): Identifier for the cell being analyzed.284    - all_paths (list of lists): Contains ordered path points for multiple paths.285    - path_lengths (list of floats): List containing lengths of each path in all_paths.286    - measured_traces (list of lists): Contains fluorescence measurement values along the paths.287    - config (dict): Configuration dictionary containing:288        - 'peak_threshold': Threshold value to determine a peak in the trace.289        - 'sphere_radius': Radius of the sphere used in fluorescence measurement.290 291    Returns:292    - pd.DataFrame: DataFrame containing peak information for each path.293    - list of lists: Absolute intensities of the detected foci.294    - list of lists: Index positions of the detected foci.295    - list of lists: Absolute focus intensity threshold for each trace.296    - list of numpy.ndarray: For each trace, distances of each point from start of trace in microns297    """298    299    n_paths = len(all_paths)300    301    data = []302    foci_absolute_intensity, foci_position, foci_position_index, screened_foci_data, trace_median_intensities, trace_thresholds = analyse_traces(all_paths, path_lengths, measured_traces, config)303 304    # Normalize foci intensities (for quantification) using trace medians as estimates of background305    foci_intensities = []306    for path_foci_abs_int, tmi in zip(foci_absolute_intensity, trace_median_intensities):307        foci_intensities.extend(list(path_foci_abs_int - tmi))308    309    # Divide all foci intensities by the mean within the cell310    mean_intensity = np.mean(foci_intensities)311    trace_positions = []312    313    for i in range(n_paths):314 315        # Calculate real (Euclidean) distance of each point along the traced path316        pl = calculate_path_length_partials(all_paths[i], (config['xy_res'], config['xy_res'], config['z_res']))317        318        319        path_data = { 'Cell_ID':cell_id,320                      'Trace': i+1,321                      'SNT_trace_length(um)': path_lengths[i],322                      'Measured_trace_length(um)': pl[-1],323                      'Trace_median_intensity': trace_median_intensities[i],324                      'Detection_sphere_radius(um)': config['sphere_radius'],325                      'Screening_distance(voxels)': config['screening_distance'],326                      'Foci_ID_threshold': config['peak_threshold'],327                      'Trace_foci_number': len(foci_position_index[i]) }328        for j, (idx, u,v) in enumerate(zip(foci_position_index[i], foci_position[i], foci_absolute_intensity[i])):329            if config['use_corrected_positions']:330                # Use the calculated position along the traced path331                path_data[f'Foci_{j+1}_position(um)'] = pl[idx]332            else:333                # Use the measured trace length (from SNT), and assume all steps of path are approximately the same length334                path_data[f'Foci_{j+1}_position(um)'] = u335            # The original measured intensity (mean in spheroid around detected peak)336            path_data[f'Foci_{j+1}_absolute_intensity'] = v337            # Measure relative intensity by removing per-trace background and dividing by cell total338            path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/mean_intensity339        data.append(path_data)340        trace_positions.append(pl)341    return pd.DataFrame(data), foci_absolute_intensity, foci_position_index, screened_foci_data, trace_thresholds, trace_positions342 343 344def analyse_paths(cell_id,345                  foci_file,346                  traces_file,347                  config348    ):349    """350    Analyzes paths for the given cell ID using provided foci and trace files.351    352    Args:353        cell_id (int/str): Identifier for the cell.354        foci_file (str): Path to the foci image file.355        traces_file (str): Path to the XML traces file.356        config (dict): Configuration dictionary containing necessary parameters such as resolutions and thresholds.357 358    Returns:359        tuple: A tuple containing an overlay image of the traces, visualization images for each trace,360               a figure with plotted measurements, and a dataframe with extracted peaks.361    """362 363 364    # Read stack365 366    foci_stack = tifffile.imread(foci_file)367 368    # If 2D add additional (z) dimension369    if foci_stack.ndim==2:370        foci_stack = foci_stack[None,:,:]371    372    all_paths, path_lengths = get_paths_from_traces_file(traces_file)373 374    all_trace_vis = [] # Per-path visualizations375    all_m = [] # Per-path measured intensities376    for p in all_paths:377        # Measure intensity along path - transpose the stack ZYX -> XYZ378        vis, m, _ = measure_chrom2(p,foci_stack.transpose(2,1,0), config)379        all_trace_vis.append(vis)380        all_m.append(m)381        382 383    # Extract all data from paths and traces384    extracted_peaks, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, all_m, config)385 386    # Plot per-path measured intensities and indicate foci387    n_cols = 2388    n_rows = (len(all_paths)+n_cols-1)//n_cols389    fig, ax = plt.subplots(n_rows,n_cols, figsize=(5*n_cols, 3*n_rows))390    ax = ax.flatten()391 392    for i, m in enumerate(all_m):393        ax[i].set_title(f'Trace {i+1}')394        ax[i].plot(trace_positions[i], m)395        if len(foci_pos_index[i]):396            # Plot detected foci397            ax[i].plot(trace_positions[i][foci_pos_index[i]], np.array(m)[foci_pos_index[i]], 'rx')398 399        if len(screened_foci_data[i]):400            # Indicate screened foci by gray circles on plots401            screened_foci_pos_index = [u.idx for u in screened_foci_data[i]]402            ax[i].plot(trace_positions[i][screened_foci_pos_index], np.array(m)[screened_foci_pos_index], color=(0.5,0.5,0.5), marker='o', linestyle='None')403 404        # Show per-trace intensity thresholds with red dotted lines405        if trace_thresholds[i] is not None:406            ax[i].axhline(trace_thresholds[i], c='r', ls=':')407        ax[i].set_xlabel('Distance from start (um)')408        ax[i].set_ylabel('Intensity')409    # Hide excess plots410    for i in range(len(all_m), n_cols*n_rows):411        ax[i].axis('off')412 413    plt.tight_layout()414    trace_overlay = draw_paths(all_paths, foci_stack, foci_index=foci_pos_index, screened_foci_data=screened_foci_data)415 416    return trace_overlay, all_trace_vis, fig, extracted_peaks417