Spaces:
Running
Running
Show screening, permit screening distance to be changed
Browse files- app.py +7 -4
- path_analysis/analyse.py +50 -26
- path_analysis/data_preprocess.py +40 -40
- tests/test_analyse.py +77 -20
- tests/test_preprocess.py +2 -2
app.py
CHANGED
|
@@ -9,6 +9,7 @@ import numpy as np
|
|
| 9 |
# Function to preview the imported image
|
| 10 |
def preview_image(file1):
|
| 11 |
if file1:
|
|
|
|
| 12 |
im = imread(file1.name)
|
| 13 |
print(im.ndim, im.shape)
|
| 14 |
if im.ndim>2:
|
|
@@ -41,6 +42,7 @@ with gr.Blocks() as demo:
|
|
| 41 |
|
| 42 |
threshold_type = gr.Radio(["per-trace", "per-cell"], label="Threshold-type", value="per-trace", interactive=True)
|
| 43 |
use_corrected_positions = gr.Checkbox(label="Correct foci position measurements", value=True, interactive=True)
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
# The output column showing the result of processing
|
|
@@ -52,15 +54,16 @@ with gr.Blocks() as demo:
|
|
| 52 |
data_file_output=gr.File(label="Output data file (.csv)")
|
| 53 |
|
| 54 |
|
| 55 |
-
def process(cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions):
|
| 56 |
|
| 57 |
config = { 'sphere_radius': sphere_radius,
|
| 58 |
'peak_threshold': peak_threshold,
|
| 59 |
'xy_res': xy_res,
|
| 60 |
'z_res': z_res,
|
| 61 |
'threshold_type': threshold_type,
|
| 62 |
-
'use_corrected_positions': use_corrected_positions
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
paths, traces, fig, extracted_peaks = analyse_paths(cellid_input, image_input.name, path_input.name, config)
|
|
@@ -71,7 +74,7 @@ with gr.Blocks() as demo:
|
|
| 71 |
|
| 72 |
with gr.Row():
|
| 73 |
greet_btn = gr.Button("Process")
|
| 74 |
-
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
| 75 |
|
| 76 |
|
| 77 |
if __name__ == "__main__":
|
|
|
|
| 9 |
# Function to preview the imported image
|
| 10 |
def preview_image(file1):
|
| 11 |
if file1:
|
| 12 |
+
print('Uploading image', file1.name)
|
| 13 |
im = imread(file1.name)
|
| 14 |
print(im.ndim, im.shape)
|
| 15 |
if im.ndim>2:
|
|
|
|
| 42 |
|
| 43 |
threshold_type = gr.Radio(["per-trace", "per-cell"], label="Threshold-type", value="per-trace", interactive=True)
|
| 44 |
use_corrected_positions = gr.Checkbox(label="Correct foci position measurements", value=True, interactive=True)
|
| 45 |
+
screening_distance = gr.Number(label='Screening distance (voxels)', value=10, interactive=True)
|
| 46 |
|
| 47 |
|
| 48 |
# The output column showing the result of processing
|
|
|
|
| 54 |
data_file_output=gr.File(label="Output data file (.csv)")
|
| 55 |
|
| 56 |
|
| 57 |
+
def process(cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions, screening_distance):
|
| 58 |
|
| 59 |
config = { 'sphere_radius': sphere_radius,
|
| 60 |
'peak_threshold': peak_threshold,
|
| 61 |
'xy_res': xy_res,
|
| 62 |
'z_res': z_res,
|
| 63 |
'threshold_type': threshold_type,
|
| 64 |
+
'use_corrected_positions': use_corrected_positions,
|
| 65 |
+
'screening_distance': screening_distance,
|
| 66 |
+
}
|
| 67 |
|
| 68 |
|
| 69 |
paths, traces, fig, extracted_peaks = analyse_paths(cellid_input, image_input.name, path_input.name, config)
|
|
|
|
| 74 |
|
| 75 |
with gr.Row():
|
| 76 |
greet_btn = gr.Button("Process")
|
| 77 |
+
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions, screening_distance], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
| 78 |
|
| 79 |
|
| 80 |
if __name__ == "__main__":
|
path_analysis/analyse.py
CHANGED
|
@@ -53,6 +53,7 @@ def calculate_path_length_partials(point_list, voxel_size=(1,1,1)):
|
|
| 53 |
section_lengths = [0.0]
|
| 54 |
s = np.array(voxel_size)
|
| 55 |
for i in range(len(point_list)-1):
|
|
|
|
| 56 |
section_lengths.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))
|
| 57 |
return np.cumsum(section_lengths)
|
| 58 |
|
|
@@ -89,7 +90,7 @@ def visualise_ordering(points_list, dim, wr=5, wc=5):
|
|
| 89 |
col_map = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255),
|
| 90 |
(255,127,0), (255, 0, 127), (127, 255, 0), (0, 255, 127), (127,0,255), (0,127,255)]
|
| 91 |
|
| 92 |
-
def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
| 93 |
"""
|
| 94 |
Draws paths on the provided image stack and overlays markers for the foci
|
| 95 |
|
|
@@ -98,7 +99,7 @@ def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
|
| 98 |
foci_stack (np.array): 3D numpy array representing the image stack.
|
| 99 |
foci_index (list, optional): List of list of focus indices (along each path). Defaults to None.
|
| 100 |
r (int, optional): Radius for the ellipse or line drawing around the focus. Defaults to 3.
|
| 101 |
-
|
| 102 |
Returns:
|
| 103 |
PIL.Image.Image: An image with the drawn paths.
|
| 104 |
"""
|
|
@@ -110,13 +111,20 @@ def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
|
| 110 |
for i, (p, col) in enumerate(zip(all_paths, cycle(col_map))):
|
| 111 |
draw.line([(u[0], u[1]) for u in p], fill=col)
|
| 112 |
draw.text((p[0][0], p[0][1]), str(i+1), fill=col)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if foci_index is not None:
|
| 114 |
for i, (idx, p, col) in enumerate(zip(foci_index, all_paths, cycle(col_map))):
|
| 115 |
if len(idx):
|
| 116 |
for j in idx:
|
| 117 |
draw.line((int(p[j][0]-r), int(p[j][1]), int(p[j][0]+r), int(p[j][1])), fill=col, width=2)
|
| 118 |
draw.line((int(p[j][0]), int(p[j][1]-r), int(p[j][0]), int(p[j][1]+r)), fill=col, width=2)
|
| 119 |
-
|
| 120 |
return im
|
| 121 |
|
| 122 |
|
|
@@ -164,8 +172,7 @@ def make_mask_s(p, melem, measure_stack):
|
|
| 164 |
#
|
| 165 |
|
| 166 |
R = [u//2 for u in melem.shape]
|
| 167 |
-
|
| 168 |
-
|
| 169 |
r, c, z = p
|
| 170 |
|
| 171 |
mask = np.zeros(melem.shape)
|
|
@@ -210,7 +217,8 @@ def make_sphere(R=5, z_scale_ratio=2.3):
|
|
| 210 |
Generate a binary representation of a sphere in 3D space.
|
| 211 |
|
| 212 |
Args:
|
| 213 |
-
R (int, optional): Radius of the sphere. Default is 5.
|
|
|
|
| 214 |
z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.
|
| 215 |
|
| 216 |
Returns:
|
|
@@ -243,25 +251,26 @@ def measure_all_with_sphere(points_list, measure_stack, op='mean', R=5, z_scale_
|
|
| 243 |
|
| 244 |
|
| 245 |
# Measure fluorescence levels along ordered skeleton
|
| 246 |
-
def measure_chrom2(path,
|
| 247 |
"""
|
| 248 |
Measure fluorescence levels along an ordered skeleton.
|
| 249 |
|
| 250 |
Args:
|
| 251 |
path (list): List of ordered path points (r, c, z).
|
| 252 |
-
|
| 253 |
config (dict): Configuration dictionary containing 'z_res', 'xy_res', and 'sphere_radius' values.
|
| 254 |
|
| 255 |
Returns:
|
| 256 |
tuple: A tuple containing the visualization, mean measurements, and max measurements along the path.
|
| 257 |
"""
|
|
|
|
| 258 |
scale_ratio = config['z_res']/config['xy_res']
|
| 259 |
sphere_xy_radius = int(math.ceil(config['sphere_radius']/config['xy_res']))
|
| 260 |
|
| 261 |
-
vis = visualise_ordering(path, dim=
|
| 262 |
|
| 263 |
-
measurements = measure_all_with_sphere(path,
|
| 264 |
-
measurements_max = measure_all_with_sphere(path,
|
| 265 |
|
| 266 |
|
| 267 |
return vis, measurements, measurements_max
|
|
@@ -290,20 +299,22 @@ def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):
|
|
| 290 |
n_paths = len(all_paths)
|
| 291 |
|
| 292 |
data = []
|
| 293 |
-
foci_absolute_intensity, foci_position, foci_position_index,
|
| 294 |
|
|
|
|
| 295 |
foci_intensities = []
|
| 296 |
for path_foci_abs_int, tmi in zip(foci_absolute_intensity, trace_median_intensities):
|
| 297 |
foci_intensities.extend(list(path_foci_abs_int - tmi))
|
| 298 |
-
|
|
|
|
| 299 |
mean_intensity = np.mean(foci_intensities)
|
| 300 |
trace_positions = []
|
| 301 |
|
| 302 |
for i in range(n_paths):
|
| 303 |
|
|
|
|
| 304 |
pl = calculate_path_length_partials(all_paths[i], (config['xy_res'], config['xy_res'], config['z_res']))
|
| 305 |
|
| 306 |
-
print(i, len(all_paths[i]), len(pl))
|
| 307 |
|
| 308 |
path_data = { 'Cell_ID':cell_id,
|
| 309 |
'Trace': i+1,
|
|
@@ -311,17 +322,23 @@ def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):
|
|
| 311 |
'Measured_trace_length(um)': pl[-1],
|
| 312 |
'Trace_median_intensity': trace_median_intensities[i],
|
| 313 |
'Detection_sphere_radius(um)': config['sphere_radius'],
|
| 314 |
-
'
|
|
|
|
|
|
|
| 315 |
for j, (idx, u,v) in enumerate(zip(foci_position_index[i], foci_position[i], foci_absolute_intensity[i])):
|
| 316 |
if config['use_corrected_positions']:
|
|
|
|
| 317 |
path_data[f'Foci_{j+1}_position(um)'] = pl[idx]
|
| 318 |
else:
|
|
|
|
| 319 |
path_data[f'Foci_{j+1}_position(um)'] = u
|
|
|
|
| 320 |
path_data[f'Foci_{j+1}_absolute_intensity'] = v
|
|
|
|
| 321 |
path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/mean_intensity
|
| 322 |
data.append(path_data)
|
| 323 |
trace_positions.append(pl)
|
| 324 |
-
return pd.DataFrame(data), foci_absolute_intensity, foci_position_index,
|
| 325 |
|
| 326 |
|
| 327 |
def analyse_paths(cell_id,
|
|
@@ -344,24 +361,29 @@ def analyse_paths(cell_id,
|
|
| 344 |
"""
|
| 345 |
|
| 346 |
|
|
|
|
|
|
|
| 347 |
foci_stack = tifffile.imread(foci_file)
|
| 348 |
|
|
|
|
| 349 |
if foci_stack.ndim==2:
|
| 350 |
foci_stack = foci_stack[None,:,:]
|
| 351 |
|
| 352 |
all_paths, path_lengths = get_paths_from_traces_file(traces_file)
|
| 353 |
|
| 354 |
-
all_trace_vis = []
|
| 355 |
-
all_m = []
|
| 356 |
for p in all_paths:
|
|
|
|
| 357 |
vis, m, _ = measure_chrom2(p,foci_stack.transpose(2,1,0), config)
|
| 358 |
all_trace_vis.append(vis)
|
| 359 |
all_m.append(m)
|
| 360 |
|
| 361 |
|
| 362 |
-
|
|
|
|
| 363 |
|
| 364 |
-
|
| 365 |
n_cols = 2
|
| 366 |
n_rows = (len(all_paths)+n_cols-1)//n_cols
|
| 367 |
fig, ax = plt.subplots(n_rows,n_cols, figsize=(5*n_cols, 3*n_rows))
|
|
@@ -371,22 +393,24 @@ def analyse_paths(cell_id,
|
|
| 371 |
ax[i].set_title(f'Trace {i+1}')
|
| 372 |
ax[i].plot(trace_positions[i], m)
|
| 373 |
if len(foci_pos_index[i]):
|
|
|
|
| 374 |
ax[i].plot(trace_positions[i][foci_pos_index[i]], np.array(m)[foci_pos_index[i]], 'rx')
|
| 375 |
|
| 376 |
-
if len(
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
ax[i].plot(trace_positions[i][
|
| 380 |
|
| 381 |
-
|
| 382 |
if trace_thresholds[i] is not None:
|
| 383 |
ax[i].axhline(trace_thresholds[i], c='r', ls=':')
|
| 384 |
ax[i].set_xlabel('Distance from start (um)')
|
| 385 |
ax[i].set_ylabel('Intensity')
|
|
|
|
| 386 |
for i in range(len(all_m), n_cols*n_rows):
|
| 387 |
ax[i].axis('off')
|
| 388 |
|
| 389 |
plt.tight_layout()
|
| 390 |
-
trace_overlay = draw_paths(all_paths, foci_stack, foci_index=foci_pos_index)
|
| 391 |
|
| 392 |
return trace_overlay, all_trace_vis, fig, extracted_peaks
|
|
|
|
| 53 |
section_lengths = [0.0]
|
| 54 |
s = np.array(voxel_size)
|
| 55 |
for i in range(len(point_list)-1):
|
| 56 |
+
# Euclidean distance between successive points
|
| 57 |
section_lengths.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))
|
| 58 |
return np.cumsum(section_lengths)
|
| 59 |
|
|
|
|
| 90 |
col_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 |
|
| 93 |
+
def 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 foci
|
| 96 |
|
|
|
|
| 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 foci
|
| 103 |
Returns:
|
| 104 |
PIL.Image.Image: An image with the drawn paths.
|
| 105 |
"""
|
|
|
|
| 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 im
|
| 129 |
|
| 130 |
|
|
|
|
| 172 |
#
|
| 173 |
|
| 174 |
R = [u//2 for u in melem.shape]
|
| 175 |
+
|
|
|
|
| 176 |
r, c, z = p
|
| 177 |
|
| 178 |
mask = np.zeros(melem.shape)
|
|
|
|
| 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:
|
|
|
|
| 251 |
|
| 252 |
|
| 253 |
# Measure fluorescence levels along ordered skeleton
|
| 254 |
+
def 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 measurement
|
| 267 |
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_max
|
|
|
|
| 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 background
|
| 305 |
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 cell
|
| 310 |
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 path
|
| 316 |
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,
|
|
|
|
| 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 path
|
| 331 |
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 length
|
| 334 |
path_data[f'Foci_{j+1}_position(um)'] = u
|
| 335 |
+
# The original measured intensity (mean in spheroid around detected peak)
|
| 336 |
path_data[f'Foci_{j+1}_absolute_intensity'] = v
|
| 337 |
+
# Measure relative intensity by removing per-trace background and dividing by cell total
|
| 338 |
path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/mean_intensity
|
| 339 |
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_positions
|
| 342 |
|
| 343 |
|
| 344 |
def analyse_paths(cell_id,
|
|
|
|
| 361 |
"""
|
| 362 |
|
| 363 |
|
| 364 |
+
# Read stack
|
| 365 |
+
|
| 366 |
foci_stack = tifffile.imread(foci_file)
|
| 367 |
|
| 368 |
+
# If 2D add additional (z) dimension
|
| 369 |
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 visualizations
|
| 375 |
+
all_m = [] # Per-path measured intensities
|
| 376 |
for p in all_paths:
|
| 377 |
+
# Measure intensity along path - transpose the stack ZYX -> XYZ
|
| 378 |
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 traces
|
| 384 |
+
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 foci
|
| 387 |
n_cols = 2
|
| 388 |
n_rows = (len(all_paths)+n_cols-1)//n_cols
|
| 389 |
fig, ax = plt.subplots(n_rows,n_cols, figsize=(5*n_cols, 3*n_rows))
|
|
|
|
| 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 foci
|
| 397 |
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 plots
|
| 401 |
+
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 lines
|
| 405 |
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 plots
|
| 410 |
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_peaks
|
path_analysis/data_preprocess.py
CHANGED
|
@@ -74,10 +74,10 @@ class RemovedPeakData(object):
|
|
| 74 |
|
| 75 |
Attributes:
|
| 76 |
idx (int): Index of peak along path
|
| 77 |
-
|
| 78 |
"""
|
| 79 |
idx: int
|
| 80 |
-
|
| 81 |
|
| 82 |
@dataclass
|
| 83 |
class PathData(object):
|
|
@@ -86,17 +86,17 @@ class PathData(object):
|
|
| 86 |
This dataclass encapsulates information about the peaks,
|
| 87 |
the defining points, the fluorescence values, and the path length of a specific path.
|
| 88 |
|
| 89 |
-
Attributes: peaks (list): List of peaks in the path (indicies of positions in points,
|
| 90 |
removed_peaks (list): List of peaks in the path which have been removed because of a nearby larger peak
|
| 91 |
points (list): List of points defining the path.
|
| 92 |
-
|
| 93 |
SC_length (float): Length of the path.
|
| 94 |
|
| 95 |
"""
|
| 96 |
peaks: list
|
| 97 |
removed_peaks: list
|
| 98 |
points: list
|
| 99 |
-
|
| 100 |
SC_length: float
|
| 101 |
|
| 102 |
@dataclass
|
|
@@ -138,7 +138,7 @@ def find_peaks2(v, distance=5, prominence=0.5):
|
|
| 138 |
return n_peaks, _
|
| 139 |
|
| 140 |
|
| 141 |
-
def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
| 142 |
"""
|
| 143 |
Process traces of cells to extract peak information and organize the data.
|
| 144 |
|
|
@@ -152,6 +152,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
| 152 |
path_lengths (list of float): List of path lengths corresponding to the provided paths.
|
| 153 |
measured_trace_fluorescence (list of list of float): A list containing fluorescence
|
| 154 |
data corresponding to each path point.
|
|
|
|
| 155 |
|
| 156 |
Returns:
|
| 157 |
CellData: An object containing organized peak and path data for a given cell.
|
|
@@ -163,17 +164,17 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
| 163 |
|
| 164 |
cell_peaks = []
|
| 165 |
|
| 166 |
-
for points,
|
| 167 |
|
| 168 |
# For peak determination normalize each trace to have mean zero and s.d. 1
|
| 169 |
-
|
| 170 |
|
| 171 |
# Find peaks - these will be further refined later
|
| 172 |
-
p,_ = find_peaks2(
|
| 173 |
peaks = np.array(p, dtype=np.int32)
|
| 174 |
|
| 175 |
# Store peak data - using original values, not normalized ones
|
| 176 |
-
peak_mean_heights = [
|
| 177 |
peak_points = [ points[u] for u in peaks ]
|
| 178 |
|
| 179 |
cell_peaks.append((peaks, peak_points, peak_mean_heights))
|
|
@@ -188,7 +189,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
| 188 |
to_thin.append(PeakData(pos=cell_peaks[k][1][u], intensity=cell_peaks[k][2][u], key=(k, u)))
|
| 189 |
|
| 190 |
# Exclude any peak with a nearby brighter peak (on any SC)
|
| 191 |
-
removed_peaks, removed_larger_peaks = thin_peaks(to_thin, return_larger_peaks=True)
|
| 192 |
|
| 193 |
# Clean up and remove these peaks
|
| 194 |
new_cell_peaks = []
|
|
@@ -206,7 +207,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
| 206 |
# What's the larger point?
|
| 207 |
idx = removed_peaks.index((path_idx, peak_idx))
|
| 208 |
larger_path, larger_idx = removed_larger_peaks[idx]
|
| 209 |
-
path_removed_peaks.append(RemovedPeakData(idx=path_peaks[peak_idx],
|
| 210 |
###
|
| 211 |
|
| 212 |
new_cell_peaks.append(path_retained_peaks)
|
|
@@ -215,15 +216,15 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
| 215 |
cell_peaks = new_cell_peaks
|
| 216 |
pd_list = []
|
| 217 |
|
| 218 |
-
# Save peak positions, absolute
|
| 219 |
for k in range(len(all_paths)):
|
| 220 |
|
| 221 |
-
points,
|
| 222 |
|
| 223 |
peaks = cell_peaks[k]
|
| 224 |
removed_peaks = removed_cell_peaks[k]
|
| 225 |
|
| 226 |
-
pd = PathData(peaks=peaks, removed_peaks=removed_peaks, points=points,
|
| 227 |
pd_list.append(pd)
|
| 228 |
|
| 229 |
cd = CellData(pathdata_list=pd_list)
|
|
@@ -235,7 +236,7 @@ alpha_max = 0.4
|
|
| 235 |
|
| 236 |
|
| 237 |
# Criterion used for identifying peak as a focus - normalized (with mean and s.d.)
|
| 238 |
-
#
|
| 239 |
def focus_criterion(pos, v, alpha=alpha_max):
|
| 240 |
"""
|
| 241 |
Identify and return positions where values in the array `v` exceed a certain threshold.
|
|
@@ -271,14 +272,14 @@ def analyse_celldata(cell_data, config):
|
|
| 271 |
- foci_rel_intensity (list): List of relative intensities for the detected foci.
|
| 272 |
- foci_pos (list): List of absolute positions of the detected foci.
|
| 273 |
- foci_pos_index (list): List of indices of the detected foci.
|
| 274 |
-
-
|
| 275 |
- trace_median_intensities (list): Per-trace median intensity
|
| 276 |
- trace_thresholds (list): Per-trace absolute threshold for calling peaks as foci
|
| 277 |
"""
|
| 278 |
foci_abs_intensity = []
|
| 279 |
foci_pos = []
|
| 280 |
foci_pos_index = []
|
| 281 |
-
|
| 282 |
trace_median_intensities = []
|
| 283 |
trace_thresholds = []
|
| 284 |
|
|
@@ -296,12 +297,11 @@ def analyse_celldata(cell_data, config):
|
|
| 296 |
|
| 297 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 298 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 299 |
-
h = np.array(path_data.
|
| 300 |
h = h - np.mean(h)
|
| 301 |
h = h/np.std(h)
|
| 302 |
# Extract foci according to criterion
|
| 303 |
foci_idx = focus_criterion(peaks, h[peaks], peak_threshold)
|
| 304 |
-
print('peaks', peaks, h[peaks], foci_idx, np.mean(path_data.o_hei10))
|
| 305 |
|
| 306 |
#
|
| 307 |
removed_peaks = path_data.removed_peaks
|
|
@@ -309,30 +309,30 @@ def analyse_celldata(cell_data, config):
|
|
| 309 |
|
| 310 |
|
| 311 |
if len(peaks):
|
| 312 |
-
trace_thresholds.append((1-peak_threshold)*np.mean(path_data.
|
| 313 |
else:
|
| 314 |
trace_thresholds.append(None)
|
| 315 |
|
| 316 |
if len(removed_peaks):
|
| 317 |
if len(peaks):
|
| 318 |
-
threshold = (1-peak_threshold)*np.mean(path_data.
|
| 319 |
else:
|
| 320 |
threshold = float('-inf')
|
| 321 |
|
| 322 |
|
| 323 |
-
removed_peak_heights = np.array(path_data.
|
| 324 |
-
|
| 325 |
|
| 326 |
-
|
| 327 |
else:
|
| 328 |
-
|
| 329 |
|
| 330 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
| 331 |
foci_pos.append(pos_abs)
|
| 332 |
-
foci_abs_intensity.append(np.array(path_data.
|
| 333 |
|
| 334 |
foci_pos_index.append(foci_idx)
|
| 335 |
-
trace_median_intensities.append(np.median(path_data.
|
| 336 |
|
| 337 |
elif threshold_type == 'per-cell':
|
| 338 |
"""
|
|
@@ -343,7 +343,7 @@ def analyse_celldata(cell_data, config):
|
|
| 343 |
|
| 344 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 345 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 346 |
-
h = np.array(path_data.
|
| 347 |
h = h - np.mean(h)
|
| 348 |
max_cell_intensity = max(max_cell_intensity, np.max(h))
|
| 349 |
|
|
@@ -352,7 +352,7 @@ def analyse_celldata(cell_data, config):
|
|
| 352 |
|
| 353 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 354 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 355 |
-
h = np.array(path_data.
|
| 356 |
h = h - np.mean(h)
|
| 357 |
|
| 358 |
foci_idx = peaks[h[peaks]>peak_threshold*max_cell_intensity]
|
|
@@ -360,33 +360,33 @@ def analyse_celldata(cell_data, config):
|
|
| 360 |
removed_peaks = path_data.removed_peaks
|
| 361 |
removed_peaks_idx = np.array([u.idx for u in removed_peaks], dtype=np.int32)
|
| 362 |
|
| 363 |
-
trace_thresholds.append(np.mean(path_data.
|
| 364 |
|
| 365 |
if len(removed_peaks):
|
| 366 |
-
threshold = np.mean(path_data.
|
| 367 |
|
| 368 |
-
removed_peak_heights = np.array(path_data.
|
| 369 |
-
|
| 370 |
|
| 371 |
-
|
| 372 |
else:
|
| 373 |
-
|
| 374 |
|
| 375 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
| 376 |
foci_pos.append(pos_abs)
|
| 377 |
-
foci_abs_intensity.append(np.array(path_data.
|
| 378 |
|
| 379 |
foci_pos_index.append(foci_idx)
|
| 380 |
-
trace_median_intensities.append(np.median(path_data.
|
| 381 |
|
| 382 |
else:
|
| 383 |
raise NotImplementedError
|
| 384 |
|
| 385 |
-
return foci_abs_intensity, foci_pos, foci_pos_index,
|
| 386 |
|
| 387 |
def analyse_traces(all_paths, path_lengths, measured_trace_fluorescence, config):
|
| 388 |
|
| 389 |
-
cd = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
| 390 |
|
| 391 |
return analyse_celldata(cd, config)
|
| 392 |
|
|
|
|
| 74 |
|
| 75 |
Attributes:
|
| 76 |
idx (int): Index of peak along path
|
| 77 |
+
screening_peak (tuple): (path_idx, position along path) for screening peak
|
| 78 |
"""
|
| 79 |
idx: int
|
| 80 |
+
screening_peak: tuple
|
| 81 |
|
| 82 |
@dataclass
|
| 83 |
class PathData(object):
|
|
|
|
| 86 |
This dataclass encapsulates information about the peaks,
|
| 87 |
the defining points, the fluorescence values, and the path length of a specific path.
|
| 88 |
|
| 89 |
+
Attributes: peaks (list): List of peaks in the path (indicies of positions in points, o_intensity).
|
| 90 |
removed_peaks (list): List of peaks in the path which have been removed because of a nearby larger peak
|
| 91 |
points (list): List of points defining the path.
|
| 92 |
+
o_intensity (list): List of (unnormalized) fluorescence intensity values along the path
|
| 93 |
SC_length (float): Length of the path.
|
| 94 |
|
| 95 |
"""
|
| 96 |
peaks: list
|
| 97 |
removed_peaks: list
|
| 98 |
points: list
|
| 99 |
+
o_intensity: list
|
| 100 |
SC_length: float
|
| 101 |
|
| 102 |
@dataclass
|
|
|
|
| 138 |
return n_peaks, _
|
| 139 |
|
| 140 |
|
| 141 |
+
def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence, dmin=10):
|
| 142 |
"""
|
| 143 |
Process traces of cells to extract peak information and organize the data.
|
| 144 |
|
|
|
|
| 152 |
path_lengths (list of float): List of path lengths corresponding to the provided paths.
|
| 153 |
measured_trace_fluorescence (list of list of float): A list containing fluorescence
|
| 154 |
data corresponding to each path point.
|
| 155 |
+
dmin (float): Distance below which brighter peaks screen less bright ones.
|
| 156 |
|
| 157 |
Returns:
|
| 158 |
CellData: An object containing organized peak and path data for a given cell.
|
|
|
|
| 164 |
|
| 165 |
cell_peaks = []
|
| 166 |
|
| 167 |
+
for points, o_intensity in zip(all_paths, measured_trace_fluorescence):
|
| 168 |
|
| 169 |
# For peak determination normalize each trace to have mean zero and s.d. 1
|
| 170 |
+
intensity_normalized = (o_intensity - np.mean(o_intensity))/np.std(o_intensity)
|
| 171 |
|
| 172 |
# Find peaks - these will be further refined later
|
| 173 |
+
p,_ = find_peaks2(intensity_normalized, distance=5, prominence=0.5*np.std(intensity_normalized))
|
| 174 |
peaks = np.array(p, dtype=np.int32)
|
| 175 |
|
| 176 |
# Store peak data - using original values, not normalized ones
|
| 177 |
+
peak_mean_heights = [ o_intensity[u] for u in peaks ]
|
| 178 |
peak_points = [ points[u] for u in peaks ]
|
| 179 |
|
| 180 |
cell_peaks.append((peaks, peak_points, peak_mean_heights))
|
|
|
|
| 189 |
to_thin.append(PeakData(pos=cell_peaks[k][1][u], intensity=cell_peaks[k][2][u], key=(k, u)))
|
| 190 |
|
| 191 |
# Exclude any peak with a nearby brighter peak (on any SC)
|
| 192 |
+
removed_peaks, removed_larger_peaks = thin_peaks(to_thin, return_larger_peaks=True, dmin=dmin)
|
| 193 |
|
| 194 |
# Clean up and remove these peaks
|
| 195 |
new_cell_peaks = []
|
|
|
|
| 207 |
# What's the larger point?
|
| 208 |
idx = removed_peaks.index((path_idx, peak_idx))
|
| 209 |
larger_path, larger_idx = removed_larger_peaks[idx]
|
| 210 |
+
path_removed_peaks.append(RemovedPeakData(idx=path_peaks[peak_idx], screening_peak=(larger_path, cell_peaks[larger_path][0][larger_idx])))
|
| 211 |
###
|
| 212 |
|
| 213 |
new_cell_peaks.append(path_retained_peaks)
|
|
|
|
| 216 |
cell_peaks = new_cell_peaks
|
| 217 |
pd_list = []
|
| 218 |
|
| 219 |
+
# Save peak positions, absolute intensity intensities, and length for each SC
|
| 220 |
for k in range(len(all_paths)):
|
| 221 |
|
| 222 |
+
points, o_intensity = all_paths[k], measured_trace_fluorescence[k]
|
| 223 |
|
| 224 |
peaks = cell_peaks[k]
|
| 225 |
removed_peaks = removed_cell_peaks[k]
|
| 226 |
|
| 227 |
+
pd = PathData(peaks=peaks, removed_peaks=removed_peaks, points=points, o_intensity=o_intensity, SC_length=path_lengths[k])
|
| 228 |
pd_list.append(pd)
|
| 229 |
|
| 230 |
cd = CellData(pathdata_list=pd_list)
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
# Criterion used for identifying peak as a focus - normalized (with mean and s.d.)
|
| 239 |
+
# intensity levels being above 0.4 time maximum peak level
|
| 240 |
def focus_criterion(pos, v, alpha=alpha_max):
|
| 241 |
"""
|
| 242 |
Identify and return positions where values in the array `v` exceed a certain threshold.
|
|
|
|
| 272 |
- foci_rel_intensity (list): List of relative intensities for the detected foci.
|
| 273 |
- foci_pos (list): List of absolute positions of the detected foci.
|
| 274 |
- foci_pos_index (list): List of indices of the detected foci.
|
| 275 |
+
- screened_foci_data (list): List of RemovedPeakData indicating positions of removed peaks and the index of the larger peak
|
| 276 |
- trace_median_intensities (list): Per-trace median intensity
|
| 277 |
- trace_thresholds (list): Per-trace absolute threshold for calling peaks as foci
|
| 278 |
"""
|
| 279 |
foci_abs_intensity = []
|
| 280 |
foci_pos = []
|
| 281 |
foci_pos_index = []
|
| 282 |
+
screened_foci_data = []
|
| 283 |
trace_median_intensities = []
|
| 284 |
trace_thresholds = []
|
| 285 |
|
|
|
|
| 297 |
|
| 298 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 299 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 300 |
+
h = np.array(path_data.o_intensity)
|
| 301 |
h = h - np.mean(h)
|
| 302 |
h = h/np.std(h)
|
| 303 |
# Extract foci according to criterion
|
| 304 |
foci_idx = focus_criterion(peaks, h[peaks], peak_threshold)
|
|
|
|
| 305 |
|
| 306 |
#
|
| 307 |
removed_peaks = path_data.removed_peaks
|
|
|
|
| 309 |
|
| 310 |
|
| 311 |
if len(peaks):
|
| 312 |
+
trace_thresholds.append((1-peak_threshold)*np.mean(path_data.o_intensity) + peak_threshold*np.max(np.array(path_data.o_intensity)[peaks]))
|
| 313 |
else:
|
| 314 |
trace_thresholds.append(None)
|
| 315 |
|
| 316 |
if len(removed_peaks):
|
| 317 |
if len(peaks):
|
| 318 |
+
threshold = (1-peak_threshold)*np.mean(path_data.o_intensity) + peak_threshold*np.max(np.array(path_data.o_intensity)[peaks])
|
| 319 |
else:
|
| 320 |
threshold = float('-inf')
|
| 321 |
|
| 322 |
|
| 323 |
+
removed_peak_heights = np.array(path_data.o_intensity)[removed_peaks_idx]
|
| 324 |
+
screened_foci_idx = np.where(removed_peak_heights>threshold)[0]
|
| 325 |
|
| 326 |
+
screened_foci_data.append([removed_peaks[i] for i in screened_foci_idx])
|
| 327 |
else:
|
| 328 |
+
screened_foci_data.append([])
|
| 329 |
|
| 330 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
| 331 |
foci_pos.append(pos_abs)
|
| 332 |
+
foci_abs_intensity.append(np.array(path_data.o_intensity)[foci_idx])
|
| 333 |
|
| 334 |
foci_pos_index.append(foci_idx)
|
| 335 |
+
trace_median_intensities.append(np.median(path_data.o_intensity))
|
| 336 |
|
| 337 |
elif threshold_type == 'per-cell':
|
| 338 |
"""
|
|
|
|
| 343 |
|
| 344 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 345 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 346 |
+
h = np.array(path_data.o_intensity)
|
| 347 |
h = h - np.mean(h)
|
| 348 |
max_cell_intensity = max(max_cell_intensity, np.max(h))
|
| 349 |
|
|
|
|
| 352 |
|
| 353 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
| 354 |
# by standard deviation - note that the latter should have no effect on the results).
|
| 355 |
+
h = np.array(path_data.o_intensity)
|
| 356 |
h = h - np.mean(h)
|
| 357 |
|
| 358 |
foci_idx = peaks[h[peaks]>peak_threshold*max_cell_intensity]
|
|
|
|
| 360 |
removed_peaks = path_data.removed_peaks
|
| 361 |
removed_peaks_idx = np.array([u.idx for u in removed_peaks], dtype=np.int32)
|
| 362 |
|
| 363 |
+
trace_thresholds.append(np.mean(path_data.o_intensity) + peak_threshold*max_cell_intensity)
|
| 364 |
|
| 365 |
if len(removed_peaks):
|
| 366 |
+
threshold = np.mean(path_data.o_intensity) + peak_threshold*max_cell_intensity
|
| 367 |
|
| 368 |
+
removed_peak_heights = np.array(path_data.o_intensity)[removed_peaks_idx]
|
| 369 |
+
screened_foci_idx = np.where(removed_peak_heights>threshold)[0]
|
| 370 |
|
| 371 |
+
screened_foci_data.append([removed_peaks[i] for i in screened_foci_idx])
|
| 372 |
else:
|
| 373 |
+
screened_foci_data.append([])
|
| 374 |
|
| 375 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
| 376 |
foci_pos.append(pos_abs)
|
| 377 |
+
foci_abs_intensity.append(np.array(path_data.o_intensity)[foci_idx])
|
| 378 |
|
| 379 |
foci_pos_index.append(foci_idx)
|
| 380 |
+
trace_median_intensities.append(np.median(path_data.o_intensity))
|
| 381 |
|
| 382 |
else:
|
| 383 |
raise NotImplementedError
|
| 384 |
|
| 385 |
+
return foci_abs_intensity, foci_pos, foci_pos_index, screened_foci_data, trace_median_intensities, trace_thresholds
|
| 386 |
|
| 387 |
def analyse_traces(all_paths, path_lengths, measured_trace_fluorescence, config):
|
| 388 |
|
| 389 |
+
cd = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence, dmin=config['screening_distance'])
|
| 390 |
|
| 391 |
return analyse_celldata(cd, config)
|
| 392 |
|
tests/test_analyse.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
|
| 2 |
import pytest
|
| 3 |
from path_analysis.analyse import *
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
from math import pi
|
| 6 |
import xml.etree.ElementTree as ET
|
|
@@ -99,7 +100,7 @@ def test_get_paths_from_traces_file():
|
|
| 99 |
def test_measure_chrom2():
|
| 100 |
# Mock data
|
| 101 |
path = [(2, 3, 4), (4, 5, 6), (9, 9, 9)] # Sample ordered path points
|
| 102 |
-
|
| 103 |
config = {
|
| 104 |
'z_res': 1,
|
| 105 |
'xy_res': 0.5,
|
|
@@ -107,7 +108,7 @@ def test_measure_chrom2():
|
|
| 107 |
}
|
| 108 |
|
| 109 |
# Function call
|
| 110 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
| 111 |
|
| 112 |
# Assertions
|
| 113 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
@@ -118,7 +119,7 @@ def test_measure_chrom2():
|
|
| 118 |
def test_measure_chrom2_z():
|
| 119 |
# Mock data
|
| 120 |
path = [(2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
| 121 |
-
_,_,
|
| 122 |
config = {
|
| 123 |
'z_res': 1,
|
| 124 |
'xy_res': 0.5,
|
|
@@ -126,7 +127,7 @@ def test_measure_chrom2_z():
|
|
| 126 |
}
|
| 127 |
|
| 128 |
# Function call
|
| 129 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
| 130 |
|
| 131 |
# Assertions
|
| 132 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
@@ -137,7 +138,7 @@ def test_measure_chrom2_z():
|
|
| 137 |
def test_measure_chrom2_z2():
|
| 138 |
# Mock data
|
| 139 |
path = [(0,0,0), (2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
| 140 |
-
_,_,
|
| 141 |
config = {
|
| 142 |
'z_res': 0.25,
|
| 143 |
'xy_res': 0.5,
|
|
@@ -145,7 +146,7 @@ def test_measure_chrom2_z2():
|
|
| 145 |
}
|
| 146 |
|
| 147 |
# Function call
|
| 148 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
| 149 |
|
| 150 |
# Assertions
|
| 151 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
@@ -283,31 +284,87 @@ def test_make_sphere_equal():
|
|
| 283 |
|
| 284 |
import pandas as pd
|
| 285 |
|
| 286 |
-
|
| 287 |
-
# 1. Test basic functionality
|
| 288 |
def test_extract_peaks_basic():
|
| 289 |
-
cell_id = 1
|
| 290 |
-
all_paths = [[[0, 0], [1, 1]]]
|
| 291 |
path_lengths = [1.41] # length of the above path
|
| 292 |
measured_traces = [[100, 200]] # fluorescence along the path
|
| 293 |
-
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'use_corrected_positions': True}
|
| 294 |
|
| 295 |
-
df,
|
| 296 |
-
|
| 297 |
-
# Now add your assertions to validate the result
|
| 298 |
assert len(df) == 1, "Expected one row in DataFrame"
|
| 299 |
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
-
# 2. Test multiple paths
|
| 303 |
def test_extract_peaks_multiple_paths():
|
| 304 |
cell_id = 1
|
| 305 |
-
all_paths = [[[0, 0], [1, 1]], [[1, 1], [2, 2]]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
path_lengths = [1.41, 1.41]
|
| 307 |
measured_traces = [[100, 200], [100, 150]]
|
| 308 |
-
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'use_corrected_positions': True}
|
| 309 |
|
| 310 |
-
df,
|
| 311 |
|
|
|
|
|
|
|
| 312 |
assert len(df) == 2, "Expected two rows in DataFrame"
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
import pytest
|
| 3 |
from path_analysis.analyse import *
|
| 4 |
+
from path_analysis.data_preprocess import RemovedPeakData
|
| 5 |
import numpy as np
|
| 6 |
from math import pi
|
| 7 |
import xml.etree.ElementTree as ET
|
|
|
|
| 100 |
def test_measure_chrom2():
|
| 101 |
# Mock data
|
| 102 |
path = [(2, 3, 4), (4, 5, 6), (9, 9, 9)] # Sample ordered path points
|
| 103 |
+
intensity = np.random.rand(10, 10, 10) # Random 3D fluorescence data
|
| 104 |
config = {
|
| 105 |
'z_res': 1,
|
| 106 |
'xy_res': 0.5,
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
# Function call
|
| 111 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
| 112 |
|
| 113 |
# Assertions
|
| 114 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
|
| 119 |
def test_measure_chrom2_z():
|
| 120 |
# Mock data
|
| 121 |
path = [(2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
| 122 |
+
_,_,intensity = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
| 123 |
config = {
|
| 124 |
'z_res': 1,
|
| 125 |
'xy_res': 0.5,
|
|
|
|
| 127 |
}
|
| 128 |
|
| 129 |
# Function call
|
| 130 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
| 131 |
|
| 132 |
# Assertions
|
| 133 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
|
| 138 |
def test_measure_chrom2_z2():
|
| 139 |
# Mock data
|
| 140 |
path = [(0,0,0), (2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
| 141 |
+
_,_,intensity = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
| 142 |
config = {
|
| 143 |
'z_res': 0.25,
|
| 144 |
'xy_res': 0.5,
|
|
|
|
| 146 |
}
|
| 147 |
|
| 148 |
# Function call
|
| 149 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
| 150 |
|
| 151 |
# Assertions
|
| 152 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
|
| 284 |
|
| 285 |
import pandas as pd
|
| 286 |
|
|
|
|
|
|
|
| 287 |
def test_extract_peaks_basic():
|
| 288 |
+
cell_id = 1 # Simple per-cell tag
|
| 289 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]]] # Single, simple path
|
| 290 |
path_lengths = [1.41] # length of the above path
|
| 291 |
measured_traces = [[100, 200]] # fluorescence along the path
|
| 292 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-cell', 'use_corrected_positions': True, 'screening_distance':10 }
|
| 293 |
|
| 294 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
| 295 |
+
|
|
|
|
| 296 |
assert len(df) == 1, "Expected one row in DataFrame"
|
| 297 |
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
| 298 |
+
assert list(df['Trace_foci_number']) == [1], "Wrong foci number"
|
| 299 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
| 300 |
+
assert foci_pos_index == [[1]]
|
| 301 |
+
assert foci_absolute_intensity == [[200]]
|
| 302 |
+
assert screened_foci_data == [[]]
|
| 303 |
+
assert trace_thresholds == [ [ 150+0.4*50] ]
|
| 304 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
| 305 |
|
|
|
|
| 306 |
def test_extract_peaks_multiple_paths():
|
| 307 |
cell_id = 1
|
| 308 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 200], [2, 2, 200]]]
|
| 309 |
+
path_lengths = [1.41, 1.41]
|
| 310 |
+
measured_traces = [[100, 200], [100, 140]]
|
| 311 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-trace', 'use_corrected_positions': True, 'screening_distance':10 }
|
| 312 |
+
|
| 313 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
assert len(df) == 2, "Expected two rows in DataFrame"
|
| 318 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
| 319 |
+
assert list(df['Trace_foci_number']) == [1,1], "Wrong foci number"
|
| 320 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
| 321 |
+
print(foci_pos_index)
|
| 322 |
+
assert list(map(list, foci_pos_index)) == [[1],[1]]
|
| 323 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[140]]
|
| 324 |
+
assert trace_thresholds == [ 150+0.4*50, 120+0.4*20 ]
|
| 325 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
| 326 |
+
assert screened_foci_data == [[],[]]
|
| 327 |
+
|
| 328 |
+
def test_extract_peaks_multiple_paths_screened():
|
| 329 |
+
cell_id = 1
|
| 330 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 2], [2, 2, 2]]]
|
| 331 |
path_lengths = [1.41, 1.41]
|
| 332 |
measured_traces = [[100, 200], [100, 150]]
|
| 333 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-trace', 'use_corrected_positions': True, 'screening_distance':10 }
|
| 334 |
|
| 335 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
| 336 |
|
| 337 |
+
|
| 338 |
+
|
| 339 |
assert len(df) == 2, "Expected two rows in DataFrame"
|
| 340 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
| 341 |
+
assert list(df['Trace_foci_number']) == [1,0], "Wrong foci number"
|
| 342 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
| 343 |
+
print(foci_pos_index)
|
| 344 |
+
assert list(map(list, foci_pos_index)) == [[1],[]]
|
| 345 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[]]
|
| 346 |
+
assert trace_thresholds == [ 150+0.4*50, None ]
|
| 347 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
| 348 |
+
assert screened_foci_data == [[],[RemovedPeakData(idx=1, screening_peak=(0,1))]]
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def test_extract_peaks_multiple_paths_per_cell():
|
| 352 |
+
cell_id = 1
|
| 353 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 200], [2, 2, 200]]]
|
| 354 |
+
path_lengths = [1.41, 1.41]
|
| 355 |
+
measured_traces = [[100, 200], [100, 140]]
|
| 356 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-cell', 'use_corrected_positions': True, 'screening_distance':10 }
|
| 357 |
+
|
| 358 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
assert len(df) == 2, "Expected two rows in DataFrame"
|
| 363 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
| 364 |
+
assert list(df['Trace_foci_number']) == [1,0], "Wrong foci number"
|
| 365 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
| 366 |
+
assert list(map(list, foci_pos_index)) == [[1],[]]
|
| 367 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[]]
|
| 368 |
+
assert trace_thresholds == [ 150+0.4*50, 120+0.4*50 ]
|
| 369 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
| 370 |
+
assert screened_foci_data == [[],[]]
|
tests/test_preprocess.py
CHANGED
|
@@ -128,8 +128,8 @@ def test_process_cell_traces_peaks(mock_data):
|
|
| 128 |
# Mock data
|
| 129 |
@pytest.fixture
|
| 130 |
def mock_celldata():
|
| 131 |
-
pathdata1 = PathData(peaks=[0, 5], points=[(0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], removed_peaks=[],
|
| 132 |
-
pathdata2 = PathData(peaks=[2], points=[(1,20,0), (1,20,10), (1,20,20) ], removed_peaks=[RemovedPeakData(0, (0,5))],
|
| 133 |
return CellData(pathdata_list=[pathdata1, pathdata2])
|
| 134 |
|
| 135 |
def test_analyse_celldata(mock_celldata):
|
|
|
|
| 128 |
# Mock data
|
| 129 |
@pytest.fixture
|
| 130 |
def mock_celldata():
|
| 131 |
+
pathdata1 = PathData(peaks=[0, 5], points=[(0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], removed_peaks=[], o_intensity=[100, 8, 3, 2, 3, 69], SC_length=2.2)
|
| 132 |
+
pathdata2 = PathData(peaks=[2], points=[(1,20,0), (1,20,10), (1,20,20) ], removed_peaks=[RemovedPeakData(0, (0,5))], o_intensity=[38, 2, 20], SC_length=2.3)
|
| 133 |
return CellData(pathdata_list=[pathdata1, pathdata2])
|
| 134 |
|
| 135 |
def test_analyse_celldata(mock_celldata):
|