OSCR

Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features.

Code ↔ Paper

7 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 7 matches
  1. [1] § Materials and methods › Uncertainty quantification ↔ uncertainty_models/utils/annotate_mouse_track.py, lines 281–359 · score 0.70 · Euclidean distance, minimum distance, contour point, mouse tracking, radius, saccade
  2. [2] § Materials and methods › Uncertainty quantification ↔ uncertainty_models/utils/annotate_mouse_track_HDweighting.py, lines 281–359 · score 0.70 · Euclidean distance, minimum distance, contour point, mouse tracking, radius, saccade
  3. [3] § Materials and methods › Segmentation uncertainty prediction ↔ uncertainty_models/classification/binary_model.py, lines 127–268 · score 0.62 · Random forest, outer, Classifiers, nested, inner, splits
  4. [4] § Materials and methods › Differences between human attention and U-net saliency ↔ uncertainty_models/assess_unet_uncertainty.py, lines 1–44 · score 0.60 · Model uncertainty, Human attention, saliency maps
  5. [5] § Materials and methods › Uncertainty quantification ↔ uncertainty_models/linear_mixed_effects/model_LMR.Rmd, lines 116–161 · score 0.58 · linear mixed, random forest, variance, models, predictors, uncertainty
  6. [6] § Materials and methods › Segmentation uncertainty prediction ↔ uncertainty_models/regression/best_model.py, lines 107–183 · score 0.56 · Random forest, outer, nested, inner, splits, trained
  7. [7] § Materials and methods › Data processing ↔ unet/scripts/segmentation_test_seg_prob_atten_alllayers.py, lines 40–89 · score 0.55 · Grad CAM, feature maps, model, segmentation

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 722 lines · 27 KB · no license · 1 match

  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Mon Apr 7 10:13:16 2025
  4. @author: nina1
  5. """
  6. # Resolution of input stimulus: 1488 x 1108
  7. import pandas as pd
  8. import os
  9. import numpy as np
  10. import matplotlib.pyplot as plt
  11. import seaborn as sns
  12. from PIL import Image
  13. from scipy.stats import pearsonr
  14. import matplotlib.colors as mcolors
  15. from skimage.measure import find_contours
  16. from scipy.spatial.distance import directed_hausdorff
  17. # Base path to the CSV files
  18. base_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data"
  19. # Base path to the png files
  20. img_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data_DMG\final_png"
  21. # Load the time-stamp information
  22. loop_info = pd.read_csv(os.path.join(base_path, "filtered_info_loop_final.csv"))
  23. body_info = pd.read_csv(os.path.join(base_path, "filtered_info_body.csv"),
  24. delimiter= ";")
  25. # List of pp_ids and stimulus combinations to exclude
  26. exclude_pp25 = loop_info['pp_id'] == 'pp25'
  27. exclude_pp28_stimulus = (loop_info['pp_id'] == 'pp28') & (loop_info['stimulus'] == '00051000_39')
  28. exclude_pp21_stimulus = (loop_info['pp_id'] == 'pp21') & (loop_info['stimulus'] == '00108000_47')
  29. exclude_pp16_stimulus = (loop_info['pp_id'] == 'pp16') & (loop_info['stimulus'] == '00023000_42')
  30. # Filter out the incomplete cases
  31. complete_cases = loop_info[~(exclude_pp25 | exclude_pp28_stimulus | exclude_pp21_stimulus | exclude_pp16_stimulus)]
  32. # Compute the HD's between the outlines
  33. def load_outline(stimulus_id, pp_id, base_path):
  34. filename = f"{stimulus_id}_Task2_{pp_id}_mouse_firstloop.csv"
  35. filepath = os.path.join(base_path, "csv", filename)
  36. try:
  37. df = pd.read_csv(filepath)
  38. return df[['x_shifted', 'y_shifted']].to_numpy()
  39. except FileNotFoundError:
  40. print(f"File not found: {filepath}")
  41. return None
  42. def hausdorff_distance(a, b):
  43. if a is None or b is None:
  44. return np.nan
  45. return max(
  46. directed_hausdorff(a, b)[0],
  47. directed_hausdorff(b, a)[0]
  48. )
  49. # Result storage
  50. results = []
  51. # Loop through each row in complete_cases
  52. for index, row in complete_cases.iterrows():
  53. stimulus_id = row['stimulus']
  54. pp_id = row['pp_id']
  55. # Load this participant's outline
  56. outline_a = load_outline(stimulus_id, pp_id, base_path)
  57. # Get other participants with same stimulus
  58. others = complete_cases[
  59. (complete_cases['stimulus'] == stimulus_id) &
  60. (complete_cases['pp_id'] != pp_id)
  61. ]
  62. for _, other_row in others.iterrows():
  63. other_pp_id = other_row['pp_id']
  64. outline_b = load_outline(stimulus_id, other_pp_id, base_path)
  65. hd = hausdorff_distance(outline_a, outline_b)
  66. results.append({
  67. 'stimulus_id': stimulus_id,
  68. 'pp_id': pp_id,
  69. 'other_pp_id': other_pp_id,
  70. 'hausdorff_distance': hd
  71. })
  72. # Convert to DataFrame
  73. hausdorff_df = pd.DataFrame(results)
  74. print(hausdorff_df.head())
  75. # Look into this df
  76. len(hausdorff_df["stimulus_id"].unique())
  77. len(hausdorff_df["pp_id"].unique())
  78. len(hausdorff_df["other_pp_id"].unique())
  79. print(hausdorff_df.shape)
  80. # Create a new column with sorted participant ID pairs
  81. hausdorff_df['pp_pair'] = hausdorff_df.apply(
  82. lambda row: tuple(sorted([row['pp_id'], row['other_pp_id']])), axis=1
  83. )
  84. # Get number of unique (stimulus_id, pp_pair) combinations
  85. num_unique_pairs_unordered = hausdorff_df[['stimulus_id', 'pp_pair']].drop_duplicates().shape[0]
  86. print("Number of unique unordered (stimulus_id, participant pair) combinations:", num_unique_pairs_unordered)
  87. # Sort the pp_id and other_pp_id columns to get unique unordered pairs
  88. hausdorff_df['pp_pair'] = hausdorff_df.apply(
  89. lambda row: tuple(sorted([row['pp_id'], row['other_pp_id']])), axis=1
  90. )
  91. # Filter out unique pairs (stimulus_id, pp_pair)
  92. unique_pairs_df = hausdorff_df[['stimulus_id', 'pp_pair', 'hausdorff_distance']].drop_duplicates()
  93. # Filter out any rows where Hausdorff distance is NaN or zero
  94. unique_pairs_df = unique_pairs_df[unique_pairs_df['hausdorff_distance'].notna() & (unique_pairs_df['hausdorff_distance'] > 0)]
  95. # Get Hausdorff distances, 1/HD, and 1/HD^2
  96. hausdorff_distances = unique_pairs_df['hausdorff_distance']
  97. inverse_hd = hausdorff_distances.max() - hausdorff_distances
  98. inverse_hd_squared = 1 / (hausdorff_distances ** 2)
  99. # Plot histograms
  100. plt.figure(figsize=(15, 5))
  101. # Histogram for Hausdorff distances
  102. plt.subplot(1, 3, 1)
  103. sns.histplot(hausdorff_distances, bins=20, kde=True, color='skyblue')
  104. plt.title('Histogram of Hausdorff Distances')
  105. plt.xlabel('Hausdorff Distance')
  106. plt.ylabel('Frequency')
  107. # Histogram for 1/HD
  108. plt.subplot(1, 3, 2)
  109. sns.histplot(inverse_hd, bins=20, kde=True, color='orange')
  110. plt.title('Histogram of 1/HD')
  111. plt.xlabel('1/HD')
  112. plt.ylabel('Frequency')
  113. # Histogram for 1/HD^2
  114. plt.subplot(1, 3, 3)
  115. sns.histplot(inverse_hd_squared, bins=20, kde=True, color='green')
  116. plt.title('Histogram of 1/HD^2')
  117. plt.xlabel('1/HD^2')
  118. plt.ylabel('Frequency')
  119. plt.tight_layout()
  120. plt.show()
  121. # File prefix
  122. file_prefix = "00078000_37"
  123. pp_id = "pp4" #or pp11 also nice or pp2 for 53_25, or 19_25 pp14
  124. # Image file
  125. image_path = os.path.join(img_path, f"{file_prefix}_t2f.png")
  126. image = Image.open(image_path)
  127. # Ground truth mask file
  128. ground_truth_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data\Mask_truthandautomated"
  129. mask_truth = np.load(os.path.join(ground_truth_path, f"{file_prefix}_truth.npy"))
  130. # Extract the contours from the mouse mask
  131. def get_contours(mask):
  132. contours = find_contours(mask, 0.5) # Get all contours
  133. return [np.array(contour) for contour in contours] # Keep separate contours
  134. contours_truth = get_contours(mask_truth)
  135. # Stack all contour points into one big array
  136. all_contour_points = np.vstack(contours_truth) # shape: (total_points, 2)
  137. contour_df = pd.DataFrame(all_contour_points[:, [1, 0]], columns=["x", "y"])
  138. # Invert the y-axis
  139. contour_df["y"] = len(mask_truth) - contour_df["y"]
  140. # Task 1 files
  141. task1_fixations = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task1_{pp_id}_fixations.csv"))
  142. task1_saccades = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task1_{pp_id}_saccades.csv"))
  143. task1_fixationmap = np.load(os.path.join(base_path, "fixation_maps", f"{file_prefix}_{pp_id}_task1_mask_normalized.npy"))
  144. # Task 2 files
  145. task2_fixations = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_fixations.csv"))
  146. task2_saccades = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_saccades.csv"))
  147. task2_mouse = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_mouse_firstloop.csv"))
  148. task2_fixationmap = np.load(os.path.join(base_path, "fixation_maps", f"{file_prefix}_{pp_id}_task2_mask_normalized.npy"))
  149. # Image annotations
  150. probability_map = np.load(os.path.join(base_path, "probabilities", f"{file_prefix}-probabilities.npy"))
  151. entropy_map = np.load(os.path.join(base_path, "entropy_maps", f"{file_prefix}_entropy.npy"))
  152. # Print what was loaded until here:
  153. # Quick check of data shapes
  154. print("Loaded files:")
  155. print(f"Task 1 Fixations: {task1_fixations.shape}")
  156. print(f"Task 1 Saccades: {task1_saccades.shape}")
  157. print(f"Task 1 Fixationmap: {task1_fixationmap.shape}, min value:",
  158. task1_fixationmap.min(),
  159. ", max value:",
  160. task1_fixationmap.max())
  161. print(f"Task 2 Fixations: {task2_fixations.shape}")
  162. print(f"Task 2 Saccades: {task2_saccades.shape}")
  163. print(f"Task 2 Mouse: {task2_mouse.shape}")
  164. print(f"Task 2 Fixationmap: {task2_fixationmap.shape}",
  165. task2_fixationmap.min(),
  166. ", max value:",
  167. task2_fixationmap.max())
  168. print(f"UNET probability map: {probability_map.shape}",
  169. probability_map.min(),
  170. ", max value:",
  171. probability_map.max())
  172. print(f"Image entropy map: {entropy_map.shape}",
  173. entropy_map.min(),
  174. ", max value:",
  175. entropy_map.max())
  176. # Also extract all attention maps, from all layers and summed up
  177. attention_map_path = os.path.join(base_path, "attention_maps")
  178. # Dictionary to hold the data if you want an alternative to globals()
  179. attention_maps_dict = {}
  180. # Loop through all matching files
  181. for fname in os.listdir(attention_map_path):
  182. if fname.startswith(file_prefix) and fname.endswith(".npy"):
  183. # Extract suffix (everything after the prefix and underscore, before .npy)
  184. suffix = fname[len(file_prefix)+1:].replace(".npy", "")
  185. full_path = os.path.join(attention_map_path, fname)
  186. # Load the file (use np.load or pd.read depending on your format)
  187. attention_maps_dict[suffix] = np.load(full_path)
  188. # If you really want to create variables like `entropy_map`, `saliency_map`, etc.:
  189. globals()[f"{suffix}_map"] = attention_maps_dict[suffix]
  190. # Print the shape
  191. print(f"UNET attention map from layer {suffix}: {attention_maps_dict[suffix].shape}",
  192. attention_maps_dict[suffix].min(),
  193. ", max value:",
  194. attention_maps_dict[suffix].max())
  195. # Limit to the first loop only (for task2)
  196. line_oi = complete_cases[(complete_cases["stimulus"] == file_prefix) & (complete_cases["pp_id"] == pp_id)]
  197. if line_oi["multiple_bodies"].item() == "yes":
  198. #THEN DEAL WITH THIS
  199. bodies_oi = body_info[(body_info["stimulus"] == file_prefix) & (body_info["pp_id"] == pp_id)]
  200. # Here select for each line the times where timestamp >= start_time <= endtime
  201. # Initialize an empty list to collect all body outline files
  202. outline_file = pd.DataFrame()
  203. # Loop through each body and filter task2_mouse accordingly
  204. for _, body_row in bodies_oi.iterrows():
  205. # Select times where timestamp >= start_time <= end_time for the body
  206. body_start_time = body_row["start_time"]
  207. body_end_time = body_row["end_time"]
  208. # Filter task2_mouse based on the body's start and end time
  209. body_outline_file = task2_mouse[["timestamp", "x_shifted", "y_shifted", "velocity"]]
  210. body_outline_file = body_outline_file[(body_outline_file["timestamp"] >= body_start_time) &
  211. (body_outline_file["timestamp"] <= body_end_time)]
  212. # Append the body outline file to the combined dataframe
  213. outline_file = pd.concat([outline_file, body_outline_file], ignore_index=True)
  214. else:
  215. start_time = line_oi["start_time"].item()
  216. end_time = line_oi["end_time"].item()
  217. # Start working on a mouse-file
  218. outline_file = task2_mouse[["timestamp", "x_shifted", "y_shifted", "velocity"]]
  219. outline_file = outline_file[(outline_file["timestamp"] >= start_time) & (outline_file["timestamp"] <= end_time)]
  220. # Extract the added fixation duration in that area for task 1
  221. # Create lists to store results
  222. fix_count_list = []
  223. fix_duration_list = []
  224. avg_saccade_amp_list = []
  225. avg_saccade_peakvelocity_list = []
  226. distance_truth_list = []
  227. # Loop over each point in the outline
  228. for _, outline_row in outline_file.iterrows():
  229. x_o = outline_row["x_shifted"]
  230. y_o = outline_row["y_shifted"]
  231. # Boolean mask: which fixations fall within a 20 pixel radius
  232. # Compute distance from center
  233. dx = task1_fixations["x_shifted"] - x_o
  234. dy = task1_fixations["y_shifted"] - y_o
  235. distance = np.sqrt(dx**2 + dy**2)
  236. # Boolean mask: fixations within radius 20
  237. nearby_fixations = task1_fixations[distance <= 20]
  238. # Count and sum duration
  239. fix_count = len(nearby_fixations)
  240. fix_duration = nearby_fixations["duration"].sum() if fix_count > 0 else 0
  241. # Store values
  242. fix_count_list.append(fix_count)
  243. fix_duration_list.append(fix_duration)
  244. # Distance from saccade start to (x_o, y_o)
  245. start_dist = np.sqrt((task1_saccades["sx_shifted"] - x_o)**2 +
  246. (task1_saccades["sy_shifted"] - y_o)**2)
  247. # Distance from saccade end to (x_o, y_o)
  248. end_dist = np.sqrt((task1_saccades["ex_shifted"] - x_o)**2 +
  249. (task1_saccades["ey_shifted"] - y_o)**2)
  250. # Compute distances from (x_o, y_o) to saccade start and end points
  251. start_dist = np.sqrt((task1_saccades["sx_shifted"] - x_o)**2 +
  252. (task1_saccades["sy_shifted"] - y_o)**2)
  253. end_dist = np.sqrt((task1_saccades["ex_shifted"] - x_o)**2 +
  254. (task1_saccades["ey_shifted"] - y_o)**2)
  255. # Boolean mask: saccades within 20 pixels (circular region)
  256. within_radius_mask = (start_dist <= 20) | (end_dist <= 20)
  257. nearby_saccades = task1_saccades[within_radius_mask]
  258. if not nearby_saccades.empty:
  259. # Use these for averages
  260. avg_amp = nearby_saccades["amplitude"].mean()
  261. avg_peakvel = nearby_saccades["peak_velocity"].mean()
  262. else:
  263. # Use closest point(s) instead
  264. min_start_dist = start_dist.min()
  265. min_end_dist = end_dist.min()
  266. # Boolean mask: saccades where start or end is at the closest distance
  267. closest_mask = (start_dist == min_start_dist) | (end_dist == min_end_dist)
  268. closest_saccades = task1_saccades[closest_mask]
  269. # Compute averages from saccades closest to (x_o, y_o)
  270. avg_amp = closest_saccades["amplitude"].mean()
  271. avg_peakvel = closest_saccades["peak_velocity"].mean()
  272. # Store values
  273. avg_saccade_amp_list.append(avg_amp)
  274. avg_saccade_peakvelocity_list.append(avg_peakvel)
  275. # Calculate the minimal distance to the points of the ground truth outline
  276. # Compute Euclidean distances to all contour points
  277. # Create point
  278. point = np.array([x_o, y_o])
  279. # Get contour points as (x, y) values
  280. contour_points = contour_df[["x", "y"]].values
  281. # Compute Euclidean distances from point to all contour points
  282. distances = np.linalg.norm(contour_points - point, axis=1)
  283. # Get the minimal distance
  284. min_distance = distances.min()
  285. # Get the minimum distance
  286. min_distance = distances.min()
  287. distance_truth_list.append(min_distance)
  288. # Add results to the outline_file DataFrame
  289. outline_file["fix_count_task1"] = fix_count_list
  290. outline_file["fix_duration_task1"] = fix_duration_list
  291. outline_file["avg_sacc_amplitude_task1"] = avg_saccade_amp_list
  292. outline_file["avg_sacc_peakvel_task1"] = avg_saccade_peakvelocity_list
  293. outline_file["distance_to_truth"] = distance_truth_list
  294. # Extract the added fixation duration in that area for task 2 (CURRENTLY FOR ENTIRE DURATION)
  295. # Create lists to store results
  296. fix_count_list = []
  297. fix_duration_list = []
  298. avg_saccade_amp_list = []
  299. avg_saccade_peakvelocity_list = []
  300. # Loop over each point in the outline
  301. for _, outline_row in outline_file.iterrows():
  302. x_o = outline_row["x_shifted"]
  303. y_o = outline_row["y_shifted"]
  304. # Boolean mask: which fixations fall within a 20 pixel radius
  305. # Compute distance from center
  306. dx = task2_fixations["x_shifted"] - x_o
  307. dy = task2_fixations["y_shifted"] - y_o
  308. distance = np.sqrt(dx**2 + dy**2)
  309. # Boolean mask: fixations within radius 20
  310. nearby_fixations = task2_fixations[distance <= 20]
  311. # Count and sum duration
  312. fix_count = len(nearby_fixations)
  313. fix_duration = nearby_fixations["duration"].sum() if fix_count > 0 else 0
  314. # Store values
  315. fix_count_list.append(fix_count)
  316. fix_duration_list.append(fix_duration)
  317. # Distance from saccade start to (x_o, y_o)
  318. start_dist = np.sqrt((task2_saccades["sx_shifted"] - x_o)**2 +
  319. (task2_saccades["sy_shifted"] - y_o)**2)
  320. # Distance from saccade end to (x_o, y_o)
  321. end_dist = np.sqrt((task2_saccades["ex_shifted"] - x_o)**2 +
  322. (task2_saccades["ey_shifted"] - y_o)**2)
  323. # Compute distances from (x_o, y_o) to saccade start and end points
  324. start_dist = np.sqrt((task2_saccades["sx_shifted"] - x_o)**2 +
  325. (task2_saccades["sy_shifted"] - y_o)**2)
  326. end_dist = np.sqrt((task2_saccades["ex_shifted"] - x_o)**2 +
  327. (task2_saccades["ey_shifted"] - y_o)**2)
  328. # Boolean mask: saccades within 20 pixels (circular region)
  329. within_radius_mask = (start_dist <= 20) | (end_dist <= 20)
  330. nearby_saccades = task2_saccades[within_radius_mask]
  331. if not nearby_saccades.empty:
  332. # Use these for averages
  333. avg_amp = nearby_saccades["amplitude"].mean()
  334. avg_peakvel = nearby_saccades["peak_velocity"].mean()
  335. else:
  336. # Use closest point(s) instead
  337. min_start_dist = start_dist.min()
  338. min_end_dist = end_dist.min()
  339. # Boolean mask: saccades where start or end is at the closest distance
  340. closest_mask = (start_dist == min_start_dist) | (end_dist == min_end_dist)
  341. closest_saccades = task2_saccades[closest_mask]
  342. # Compute averages from saccades closest to (x_o, y_o)
  343. avg_amp = closest_saccades["amplitude"].mean()
  344. avg_peakvel = closest_saccades["peak_velocity"].mean()
  345. # Store values
  346. avg_saccade_amp_list.append(avg_amp)
  347. avg_saccade_peakvelocity_list.append(avg_peakvel)
  348. # Add results to the outline_file DataFrame
  349. outline_file["fix_count_task2"] = fix_count_list
  350. outline_file["fix_duration_task2"] = fix_duration_list
  351. outline_file["avg_sacc_amplitude_task2"] = avg_saccade_amp_list
  352. outline_file["avg_sacc_peakvel_task2"] = avg_saccade_peakvelocity_list
  353. # Assess the number of NAs per column (there should not be any):
  354. column_nan_count = outline_file.isnull().sum()
  355. print("NaN count per column:")
  356. print(column_nan_count)
  357. # ADD information from the maps
  358. radius = 20
  359. diameter = radius * 2 + 1
  360. # Step 1: Create a circular mask
  361. yy, xx = np.ogrid[-radius:radius+1, -radius:radius+1]
  362. circle_mask = (xx**2 + yy**2) <= radius**2
  363. # Prepare output lists
  364. avg_task1_fixation = []
  365. avg_task2_fixation = []
  366. avg_entropy = []
  367. avg_attention_maps = {name: [] for name in attention_maps_dict} # dict of {name: 2D array}
  368. # Step 2: Loop over outline points
  369. for _, row in outline_file.iterrows():
  370. x_o = int(round(row["x_shifted"]))
  371. y_o = int(round(row["y_shifted"]))
  372. # Define bounding box
  373. x_start, x_end = x_o - radius, x_o + radius + 1
  374. y_start, y_end = y_o - radius, y_o + radius + 1
  375. # Helper to extract and mask the region
  376. def extract_avg(img):
  377. h, w = img.shape
  378. if x_start < 0 or y_start < 0 or x_end > w or y_end > h:
  379. return np.nan # out of bounds
  380. region = img[y_start:y_end, x_start:x_end]
  381. return region[circle_mask].mean()
  382. avg_task1_fixation.append(extract_avg(task1_fixationmap))
  383. avg_task2_fixation.append(extract_avg(task2_fixationmap))
  384. avg_entropy.append(extract_avg(entropy_map))
  385. for name, amap in attention_maps_dict.items():
  386. avg_attention_maps[name].append(extract_avg(amap))
  387. # Step 3: Add to DataFrame
  388. outline_file["avg_task1_fix"] = avg_task1_fixation
  389. outline_file["avg_task2_fix"] = avg_task2_fixation
  390. outline_file["avg_entropy"] = avg_entropy
  391. for name, values in avg_attention_maps.items():
  392. outline_file[f"avg_{name}"] = values
  393. # Assess the number of NAs per column (there should not be any):
  394. column_nan_count = outline_file.isnull().sum()
  395. print("NaN count per column:")
  396. print(column_nan_count)
  397. # Add interobserver variability, consider only the participants with DICE >0.7
  398. # Before this: 143 pp-stimuli pairs
  399. # After this: still 94 pp-stimuli pairs
  400. # 1. Filter loop_info_first for DICE > 0.7
  401. loop_info_highDICE = loop_info_first[loop_info_first["DICE_truth"] > 0.7]
  402. # 2. Get participant IDs for same stimulus and different pp_id
  403. pp_ids_oi = loop_info_highDICE["pp_id"][
  404. (loop_info_highDICE["stimulus"] == file_prefix) &
  405. (loop_info_highDICE["pp_id"] != pp_id)
  406. ]
  407. # 3. Load all comparison outlines for this stimulus
  408. other_pp_outlines = []
  409. for pp in pp_ids_oi:
  410. file_path = os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp}_mouse_firstloop.csv")
  411. if os.path.exists(file_path):
  412. df = pd.read_csv(file_path)
  413. other_pp_outlines.append(df[["x_shifted", "y_shifted"]].values)
  414. # 4. Compute variability for each point in the current outline
  415. variability_list = []
  416. for _, outline_row in outline_file.iterrows():
  417. x_o = outline_row["x_shifted"]
  418. y_o = outline_row["y_shifted"]
  419. point = np.array([x_o, y_o])
  420. # Store all minimal distances to this point across other participant outlines
  421. min_dists = []
  422. for other_outline in other_pp_outlines:
  423. # Euclidean distances to all points in this other outline
  424. dists = np.linalg.norm(other_outline - point, axis=1)
  425. min_dists.append(dists.min()) # Closest point on this outline
  426. # Median across all other outlines
  427. if min_dists:
  428. variability_list.append(np.median(min_dists))
  429. else:
  430. variability_list.append(np.nan)
  431. # 5. Add to DataFrame
  432. outline_file["variability_median_min_dist"] = variability_list
  433. # Select only continuous variables (excluding 'timestamp', 'x_shifted', 'y_shifted')
  434. continuous_vars = outline_file.drop(columns=['timestamp', 'x_shifted', 'y_shifted'])
  435. # Step 1: Compute the correlation matrix
  436. corr_matrix = continuous_vars.corr()
  437. # Step 2: Compute p-values for correlation significance
  438. def compute_p_value(x, y):
  439. return pearsonr(x, y)[1]
  440. p_values = continuous_vars.corr(method=lambda x, y: compute_p_value(x, y)) # Get p-values
  441. # Step 3: Handle missing data for saccade columns by excluding rows with NaN in these columns
  442. # Create a mask for NaN in saccade columns (fixation columns are kept as is)
  443. mask_saccades = continuous_vars[['avg_sacc_amplitude_task1', 'avg_sacc_peakvel_task1',
  444. 'avg_sacc_amplitude_task2', 'avg_sacc_peakvel_task2']].notna()
  445. # Apply mask only to saccade-related columns
  446. mask = mask_saccades.all(axis=1)
  447. filtered_continuous_vars = continuous_vars[mask]
  448. # Recompute correlation matrix and p-values after filtering
  449. corr_matrix_filtered = filtered_continuous_vars.corr()
  450. p_values_filtered = filtered_continuous_vars.corr(method=lambda x, y: compute_p_value(x, y))
  451. # Step 4: Define significance stars function
  452. def significance_stars(p):
  453. if p < 0.001:
  454. return "***"
  455. elif p < 0.01:
  456. return "**"
  457. elif p < 0.05:
  458. return "*"
  459. else:
  460. return ""
  461. # Step 5: Generate the annotation for the heatmap
  462. # Format correlation values and add significance stars
  463. annot = p_values_filtered.applymap(significance_stars)
  464. #corr_annot = corr_matrix_filtered.round(2).astype(str) + annot
  465. corr_annot = annot # only significance start
  466. # Step 6: Plot the heatmap
  467. plt.figure(figsize=(12, 10))
  468. sns.heatmap(corr_matrix_filtered, annot=corr_annot, fmt="", cmap="coolwarm", center=0, linewidths=0.5, vmin=-1, vmax=1)
  469. # Title
  470. plt.title("Correlation Heatmap with Significance Levels (***: p < 0.001, **: p < 0.01. *: p < 0.05)", fontsize=14)
  471. plt.show()
  472. # Plot this information
  473. plt.scatter(outline_file["fix_count_task1"], outline_file["fix_count_task2"])
  474. # Add labels and title for clarity
  475. plt.xlabel('Fixation Count (Task 1)')
  476. plt.ylabel('Fixation Count (Task 2)')
  477. # Display the plot
  478. plt.show()
  479. # Function to plot saccades, fixations, and mouse track annotations
  480. def plot_saccades_and_fixations(image, saccades, fixations, title="Plot", output_path=None):
  481. # Create a figure and axis for the plot
  482. fig, ax = plt.subplots(figsize=(8, 8))
  483. ax.imshow(image) # Display the image
  484. # Plot saccades
  485. for _, row in saccades.iterrows():
  486. sx, sy, ex, ey = row['sx_shifted'], row['sy_shifted'], row['ex_shifted'], row['ey_shifted']
  487. ax.plot([sx, ex], [sy, ey],
  488. color='gold',
  489. alpha = 0.5,
  490. linewidth=1)
  491. # Plot fixations
  492. for _, row in fixations.iterrows():
  493. x, y, duration = row['x_shifted'], row['y_shifted'], row['duration']
  494. radius = np.sqrt(duration) * 0.5
  495. circle = plt.Circle((x, y), radius, color='violet', alpha=0.9, fill=False, linewidth=1.5)
  496. ax.add_patch(circle)
  497. # Title and axis settings
  498. ax.set_title(title, fontsize=14)
  499. ax.axis("off")
  500. # Save or display the plot
  501. #if output_path:
  502. #plt.savefig(output_path, bbox_inches='tight')
  503. plt.show()
  504. # Plot for task1
  505. plot_saccades_and_fixations(
  506. image=image, # Load your image here
  507. saccades=task1_saccades, # Load saccades dataframe here
  508. fixations=task1_fixations, # Load fixations dataframe here
  509. title="Saccades and Fixations Visualization (task 1)",
  510. output_path=None # If you want to save, provide the path
  511. )
  512. # Plot for task2
  513. plot_saccades_and_fixations(
  514. image=image, # Load your image here
  515. saccades=task2_saccades, # Load saccades dataframe here
  516. fixations=task2_fixations, # Load fixations dataframe here
  517. title="Saccades and Fixations Visualization (task2)",
  518. output_path=None # If you want to save, provide the path
  519. )
  520. # Create a truncated version of the magma colormap
  521. def truncate_colormap(cmap, minval=0.1, maxval=1.0, n=100):
  522. """
  523. Truncates a colormap so that it starts at minval and ends at maxval.
  524. """
  525. new_cmap = mcolors.LinearSegmentedColormap.from_list(
  526. f"truncated_{cmap.name}", cmap(np.linspace(minval, maxval, n))
  527. )
  528. return new_cmap
  529. # Use magma colormap and truncate it
  530. cmap = plt.get_cmap('magma')
  531. cmap_truncated = truncate_colormap(cmap, minval=0.2) # Adjust minval as needed to start from purple
  532. # Function to plot mouse track annotations
  533. def plot_mouse_track_annotations(image, outline_file, annot_column, title="Plot", output_path=None):
  534. # Create a figure and axis for the plot
  535. fig, ax = plt.subplots(figsize=(8, 8))
  536. ax.imshow(image) # Display the image
  537. # Normalize the annotation column values for coloring
  538. norm = plt.Normalize(vmin=outline_file[annot_column].min(), vmax=outline_file[annot_column].max())
  539. cmap = cmap_truncated # You can use other color maps like 'coolwarm', 'plasma', etc.
  540. # Plot mouse track (annotate by the specified column)
  541. scatter = ax.scatter(outline_file['x_shifted'], outline_file['y_shifted'],
  542. c=outline_file[annot_column], cmap=cmap, norm=norm, s=10, alpha=0.7)
  543. # Add a smaller colorbar to the plot for better understanding of the metric values
  544. cbar = plt.colorbar(scatter, ax=ax, orientation='vertical', fraction=0.02, pad=0.04) # Adjust fraction and pad
  545. cbar.set_label(annot_column, fontsize=10)
  546. # Set the title to include the annotation column
  547. ax.set_title(f"{title} - Annotated by {annot_column}", fontsize=14)
  548. # Remove axes
  549. ax.axis("off")
  550. # Save or display the plot
  551. #if output_path:
  552. #plt.savefig(output_path, bbox_inches='tight')
  553. plt.show()
  554. # Function to plot mouse track annotations for all columns in outline_file
  555. def plot_for_all_columns(image, outline_file, title="Mouse Track with Annotations", output_path=None):
  556. # Iterate over each column in outline_file (excluding columns like 'x_shifted', 'y_shifted')
  557. for column in outline_file.columns:
  558. # Skip the columns that are not suitable for annotation (e.g., 'x_shifted', 'y_shifted', etc.)
  559. if column in ['x_shifted', 'y_shifted', 'timestamp']: # Adjust as necessary
  560. continue
  561. # Call the plot function for each column
  562. plot_mouse_track_annotations(
  563. image=image,
  564. outline_file=outline_file,
  565. annot_column=column,
  566. title=f"{title} - Annotated by {column}",
  567. output_path=output_path # You can provide a path to save the plot
  568. )
  569. # Example usage
  570. plot_for_all_columns(
  571. image=image, # Load your image here
  572. outline_file=outline_file, # Load outline_file dataframe here
  573. title="Mouse Track with Annotations", # Title for the plot
  574. output_path=None # If you want to save, provide the path
  575. )

annotate_mouse_track.py at commit 4bd8e5f, no license · at the source

Overview

Authors: Nina Baumgartner1, Daria Laslo1,2, Laura Fontanesi3, Catherine R Jutzeler1,2, Arzu Çöltekin3, Sarah Brüningk4,5
  1. ETH Zurich, Rämistrasse101, 8092 Zürich, Switzerland
  2. Swiss Institute of Bioinformatics, Lagerstrasse 5, Lausanne, Switzerland
  3. Fachhochschule Nordwestschweiz FHNW, Hochschule für Informatik, 5210 Windisch, Switzerland
  4. Department of Radiation Oncology, Inselspital, Bern University Hospital and University of Bern, Switzerland
  5. Department of Digital Medicine, University of Bern, Bern, Switzerland
Journal: Physics and imaging in radiation oncology, volume 40, article 101056
Dates: received 12 February 2026; accepted 3 August 2026; published online 4 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.phro.2026.101056 · PMID 42630840 · PMCID PMC13495590 · OpenAlex W7172425191
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism), other condition (population)
Methods: Connectivity, Statistics, Machine learning, Graphs, Physiology & signal measures
Keywords: Tumour segmentation, Segmentation uncertainty, Eye-tracking, Mouse-tracking, Paediatric brain tumour
Topic: Gaze Tracking and Assistive Technology (Human-Computer Interaction, Computer Science), according to OpenAlex
Funding: Swiss National Science Foundation (225913)
Citations: not cited yet (Europe PMC); 46 references in the paper

Abstract

Background and Purpose: Accurate tumour delineation is key in radiotherapy workflows. Concurrently, diffuse tumour types are inherently difficult to delineate, rendering contour uncertainty information highly valuable for downstream treatment planning. In this study, we investigated whether uncertainty in manually delineated tumour contours can be inferred from clinician behaviour (eye and mouse movements) and image-derived indicators.

Materials and Methods: In a two-stage controlled experiment, 36 clinical imaging experts described and manually delineated brain tumours on T2-FLAIR (fluid-attenuated inversion recovery) MRI scans, while their eye and mouse movements were recorded. Inter-observer contour variability was used as a proxy for contour uncertainty. In addition, we extracted image-derived features, including U-Net saliency maps, pixel-wise U-Net segmentation probabilities, and image entropy. To assess and predict contour uncertainty, we applied mixed-effects models and machine learning algorithms.

Results: Higher contour uncertainty was associated with higher saccade velocities and greater fixation density. Uncertainty was also significantly correlated with segmentation error and image-derived features (p < 0.05). A random forest regressor, combining behavioural and image-derived features, explained 39% of the variance in contour uncertainty. Notably, features derived from downsampling layers were the strongest predictors, despite displaying the lowest saliency overlap with human attention.

Conclusions: Our results suggest that uncertainty in manually delineated tumour contours can be estimated using behavioural and image-derived features, without explicit uncertainty annotations. While clinical deployment will require validation under realistic acquisition conditions and with specialty experts, these findings established the feasibility of passively inferred uncertainty as a foundation for future uncertainty-aware delineation tools.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 7 matches between paragraphs and lines of code.

gitlab.ethz.ch/bmdslab/publications/oncology/manual-segmentation-uncertainty-using-eyetracking

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 4bd8e5f9b43bf98c89833865c088dc2254f99204, 29 May 2026
Languages: Python (29), Shell (3), R (1)
Size: 36 files, 33 scripts
Software Heritage: not archived
Found in: the text, “Differences between human attention and U-net sa”
Holds: README, environment (environment.yml), 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (26 files), Matplotlib (20 files), pandas (19 files), SciPy (12 files), Pillow (9 files), scikit-image (9 files), seaborn (9 files), NiBabel (8 files), PyTorch (5 files), scikit-learn (4 files), XGBoost (2 files), brms (1 file), car (1 file), easystats (1 file), lme4 (1 file), lmerTest (1 file), OpenCV (1 file), randomForest (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
34 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 33 scripts, each with its path and the digest of its content;
  • 7 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data sharing

Imaging data is publicly available at https://synapse.org/Synapse:syn51156910 upon request. The acquired eye and mouse tracking data is available upon request from the senior authors (contact Prof. Sarah Brüningk, University of Bern, ; Prof. Arzu Coltekin, University of Applied Sciences and Arts Northwestern Switzerland, ).

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 6 authors, 5 keywords, 1 funder, 39 references.

Cite

This paper

Baumgartner, N., Laslo, D., Fontanesi, L., Jutzeler, C. R., Çöltekin, A., & Brüningk, S. (2026). Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features. Physics and imaging in radiation oncology, 40, 101056. https://doi.org/10.1016/j.phro.2026.101056

BibTeX

@article{baumgartner2026toward,
author = {Baumgartner, Nina and Laslo, Daria and Fontanesi, Laura and Jutzeler, Catherine R and Çöltekin, Arzu and Brüningk, Sarah},
title = {{Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features}},
journal = {Physics and imaging in radiation oncology},
year = {2026},
month = jul,
volume = {40},
pages = {101056},
publisher = {Elsevier},
issn = {2405-6316},
doi = {10.1016/j.phro.2026.101056},
url = {https://doi.org/10.1016/j.phro.2026.101056},
pmid = {42630840},
pmcid = {PMC13495590}
}

RIS

TY - JOUR
AU - Baumgartner, Nina
AU - Laslo, Daria
AU - Fontanesi, Laura
AU - Jutzeler, Catherine R
AU - Çöltekin, Arzu
AU - Brüningk, Sarah
TI - Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features
T2 - Physics and imaging in radiation oncology
J2 - Phys Imaging Radiat Oncol
PY - 2026
DA - 2026/07/01
VL - 40
SP - 101056
SN - 2405-6316
PB - Elsevier
DO - 10.1016/j.phro.2026.101056
UR - https://doi.org/10.1016/j.phro.2026.101056
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.phro.2026.101056",
"type": "article-journal",
"title": "Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features",
"container-title": "Physics and imaging in radiation oncology",
"author": [
{
"family": "Baumgartner",
"given": "Nina"
},
{
"family": "Laslo",
"given": "Daria"
},
{
"family": "Fontanesi",
"given": "Laura"
},
{
"family": "Jutzeler",
"given": "Catherine R"
},
{
"family": "Çöltekin",
"given": "Arzu"
},
{
"family": "Brüningk",
"given": "Sarah"
}
],
"container-title-short": "Phys Imaging Radiat Oncol",
"volume": "40",
"page": "101056",
"DOI": "10.1016/j.phro.2026.101056",
"PMID": "42630840",
"PMCID": "PMC13495590",
"ISSN": "2405-6316",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.phro.2026.101056",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: randomForest, brms, XGBoost, 12 other tools, mouse
[2] doi:10.1038/s41598-026-48496-1 [code]
A unified FLAIR hyperintensity segmentation model for various CNS tumor types and acquisition time points.
Journal: Scientific reports
In common: scikit-image, Pillow, NiBabel, 6 other tools, synapse.org/synapse:syn51156910, other condition, 2 references
[3] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: car, lmerTest, lme4, 10 other tools, other condition
[4] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: XGBoost, car, lmerTest, 9 other tools, other condition
[5] doi:10.34133/csbj.0042 [code]
Using Steady-State Visual Evoked Potentials to Characterize Wide-Ranging Retinopathy Linked to &lt;i&gt;CRB1&lt;/i&gt;: Implications for Clinical Trials.
Journal: Computational and structural biotechnology journal
In common: easystats, car, lmerTest, 9 other tools, other condition
[6] doi:10.1093/braincomms/fcag176 [code]
Tau topography subtypes account for clinical heterogeneity and longitudinal trajectories in early-onset Alzheimer's disease.
Journal: Brain communications
In common: easystats, car, lmerTest, 9 other tools
[7] doi:10.1038/s41467-026-73865-9 [code]
Histamine shapes the neurocomputational dynamics of human learning.
Journal: Nature communications
In common: brms, easystats, car, 8 other tools
[8] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: easystats, car, lmerTest, 9 other tools
[9] doi:10.1162/imag.a.1347 [code]
Neural and behavioural correlates of theory of mind reasoning in five-year-old children born preterm.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: brms, easystats, lmerTest, 8 other tools, other condition
[10] doi:10.1038/s41467-026-76837-1 [code]
Drug screen and machine learning predict neuroprotective agents in a preclinical human model of childhood dementia.
Journal: Nature communications
In common: XGBoost, OpenCV, scikit-image, 9 other tools, other condition

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.