Toward uncertainty-aware manual delineation of brain tumours using eye-tracking and image-derived features.
The 7 matches
- [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] § 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] § 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] § 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] § 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] § 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] § 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
- # -*- coding: utf-8 -*-
- """
- Created on Mon Apr 7 10:13:16 2025
- @author: nina1
- """
- # Resolution of input stimulus: 1488 x 1108
- import pandas as pd
- import os
- import numpy as np
- import matplotlib.pyplot as plt
- import seaborn as sns
- from PIL import Image
- from scipy.stats import pearsonr
- import matplotlib.colors as mcolors
- from skimage.measure import find_contours
- from scipy.spatial.distance import directed_hausdorff
- # Base path to the CSV files
- base_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data"
- # Base path to the png files
- img_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data_DMG\final_png"
- # Load the time-stamp information
- loop_info = pd.read_csv(os.path.join(base_path, "filtered_info_loop_final.csv"))
- body_info = pd.read_csv(os.path.join(base_path, "filtered_info_body.csv"),
- delimiter= ";")
- # List of pp_ids and stimulus combinations to exclude
- exclude_pp25 = loop_info['pp_id'] == 'pp25'
- exclude_pp28_stimulus = (loop_info['pp_id'] == 'pp28') & (loop_info['stimulus'] == '00051000_39')
- exclude_pp21_stimulus = (loop_info['pp_id'] == 'pp21') & (loop_info['stimulus'] == '00108000_47')
- exclude_pp16_stimulus = (loop_info['pp_id'] == 'pp16') & (loop_info['stimulus'] == '00023000_42')
- # Filter out the incomplete cases
- complete_cases = loop_info[~(exclude_pp25 | exclude_pp28_stimulus | exclude_pp21_stimulus | exclude_pp16_stimulus)]
- # Compute the HD's between the outlines
- def load_outline(stimulus_id, pp_id, base_path):
- filename = f"{stimulus_id}_Task2_{pp_id}_mouse_firstloop.csv"
- filepath = os.path.join(base_path, "csv", filename)
- try:
- df = pd.read_csv(filepath)
- return df[['x_shifted', 'y_shifted']].to_numpy()
- except FileNotFoundError:
- print(f"File not found: {filepath}")
- return None
- def hausdorff_distance(a, b):
- if a is None or b is None:
- return np.nan
- return max(
- directed_hausdorff(a, b)[0],
- directed_hausdorff(b, a)[0]
- )
- # Result storage
- results = []
- # Loop through each row in complete_cases
- for index, row in complete_cases.iterrows():
- stimulus_id = row['stimulus']
- pp_id = row['pp_id']
- # Load this participant's outline
- outline_a = load_outline(stimulus_id, pp_id, base_path)
- # Get other participants with same stimulus
- others = complete_cases[
- (complete_cases['stimulus'] == stimulus_id) &
- (complete_cases['pp_id'] != pp_id)
- ]
- for _, other_row in others.iterrows():
- other_pp_id = other_row['pp_id']
- outline_b = load_outline(stimulus_id, other_pp_id, base_path)
- hd = hausdorff_distance(outline_a, outline_b)
- results.append({
- 'stimulus_id': stimulus_id,
- 'pp_id': pp_id,
- 'other_pp_id': other_pp_id,
- 'hausdorff_distance': hd
- })
- # Convert to DataFrame
- hausdorff_df = pd.DataFrame(results)
- print(hausdorff_df.head())
- # Look into this df
- len(hausdorff_df["stimulus_id"].unique())
- len(hausdorff_df["pp_id"].unique())
- len(hausdorff_df["other_pp_id"].unique())
- print(hausdorff_df.shape)
- # Create a new column with sorted participant ID pairs
- hausdorff_df['pp_pair'] = hausdorff_df.apply(
- lambda row: tuple(sorted([row['pp_id'], row['other_pp_id']])), axis=1
- )
- # Get number of unique (stimulus_id, pp_pair) combinations
- num_unique_pairs_unordered = hausdorff_df[['stimulus_id', 'pp_pair']].drop_duplicates().shape[0]
- print("Number of unique unordered (stimulus_id, participant pair) combinations:", num_unique_pairs_unordered)
- # Sort the pp_id and other_pp_id columns to get unique unordered pairs
- hausdorff_df['pp_pair'] = hausdorff_df.apply(
- lambda row: tuple(sorted([row['pp_id'], row['other_pp_id']])), axis=1
- )
- # Filter out unique pairs (stimulus_id, pp_pair)
- unique_pairs_df = hausdorff_df[['stimulus_id', 'pp_pair', 'hausdorff_distance']].drop_duplicates()
- # Filter out any rows where Hausdorff distance is NaN or zero
- unique_pairs_df = unique_pairs_df[unique_pairs_df['hausdorff_distance'].notna() & (unique_pairs_df['hausdorff_distance'] > 0)]
- # Get Hausdorff distances, 1/HD, and 1/HD^2
- hausdorff_distances = unique_pairs_df['hausdorff_distance']
- inverse_hd = hausdorff_distances.max() - hausdorff_distances
- inverse_hd_squared = 1 / (hausdorff_distances ** 2)
- # Plot histograms
- plt.figure(figsize=(15, 5))
- # Histogram for Hausdorff distances
- plt.subplot(1, 3, 1)
- sns.histplot(hausdorff_distances, bins=20, kde=True, color='skyblue')
- plt.title('Histogram of Hausdorff Distances')
- plt.xlabel('Hausdorff Distance')
- plt.ylabel('Frequency')
- # Histogram for 1/HD
- plt.subplot(1, 3, 2)
- sns.histplot(inverse_hd, bins=20, kde=True, color='orange')
- plt.title('Histogram of 1/HD')
- plt.xlabel('1/HD')
- plt.ylabel('Frequency')
- # Histogram for 1/HD^2
- plt.subplot(1, 3, 3)
- sns.histplot(inverse_hd_squared, bins=20, kde=True, color='green')
- plt.title('Histogram of 1/HD^2')
- plt.xlabel('1/HD^2')
- plt.ylabel('Frequency')
- plt.tight_layout()
- plt.show()
- # File prefix
- file_prefix = "00078000_37"
- pp_id = "pp4" #or pp11 also nice or pp2 for 53_25, or 19_25 pp14
- # Image file
- image_path = os.path.join(img_path, f"{file_prefix}_t2f.png")
- image = Image.open(image_path)
- # Ground truth mask file
- ground_truth_path = r"C:\Users\nina1\Documents\aaETH\MasterThesis\thesis-repo\Project_2\Data\Mask_truthandautomated"
- mask_truth = np.load(os.path.join(ground_truth_path, f"{file_prefix}_truth.npy"))
- # Extract the contours from the mouse mask
- def get_contours(mask):
- contours = find_contours(mask, 0.5) # Get all contours
- return [np.array(contour) for contour in contours] # Keep separate contours
- contours_truth = get_contours(mask_truth)
- # Stack all contour points into one big array
- all_contour_points = np.vstack(contours_truth) # shape: (total_points, 2)
- contour_df = pd.DataFrame(all_contour_points[:, [1, 0]], columns=["x", "y"])
- # Invert the y-axis
- contour_df["y"] = len(mask_truth) - contour_df["y"]
- # Task 1 files
- task1_fixations = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task1_{pp_id}_fixations.csv"))
- task1_saccades = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task1_{pp_id}_saccades.csv"))
- task1_fixationmap = np.load(os.path.join(base_path, "fixation_maps", f"{file_prefix}_{pp_id}_task1_mask_normalized.npy"))
- # Task 2 files
- task2_fixations = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_fixations.csv"))
- task2_saccades = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_saccades.csv"))
- task2_mouse = pd.read_csv(os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp_id}_mouse_firstloop.csv"))
- task2_fixationmap = np.load(os.path.join(base_path, "fixation_maps", f"{file_prefix}_{pp_id}_task2_mask_normalized.npy"))
- # Image annotations
- probability_map = np.load(os.path.join(base_path, "probabilities", f"{file_prefix}-probabilities.npy"))
- entropy_map = np.load(os.path.join(base_path, "entropy_maps", f"{file_prefix}_entropy.npy"))
- # Print what was loaded until here:
- # Quick check of data shapes
- print("Loaded files:")
- print(f"Task 1 Fixations: {task1_fixations.shape}")
- print(f"Task 1 Saccades: {task1_saccades.shape}")
- print(f"Task 1 Fixationmap: {task1_fixationmap.shape}, min value:",
- task1_fixationmap.min(),
- ", max value:",
- task1_fixationmap.max())
- print(f"Task 2 Fixations: {task2_fixations.shape}")
- print(f"Task 2 Saccades: {task2_saccades.shape}")
- print(f"Task 2 Mouse: {task2_mouse.shape}")
- print(f"Task 2 Fixationmap: {task2_fixationmap.shape}",
- task2_fixationmap.min(),
- ", max value:",
- task2_fixationmap.max())
- print(f"UNET probability map: {probability_map.shape}",
- probability_map.min(),
- ", max value:",
- probability_map.max())
- print(f"Image entropy map: {entropy_map.shape}",
- entropy_map.min(),
- ", max value:",
- entropy_map.max())
- # Also extract all attention maps, from all layers and summed up
- attention_map_path = os.path.join(base_path, "attention_maps")
- # Dictionary to hold the data if you want an alternative to globals()
- attention_maps_dict = {}
- # Loop through all matching files
- for fname in os.listdir(attention_map_path):
- if fname.startswith(file_prefix) and fname.endswith(".npy"):
- # Extract suffix (everything after the prefix and underscore, before .npy)
- suffix = fname[len(file_prefix)+1:].replace(".npy", "")
- full_path = os.path.join(attention_map_path, fname)
- # Load the file (use np.load or pd.read depending on your format)
- attention_maps_dict[suffix] = np.load(full_path)
- # If you really want to create variables like `entropy_map`, `saliency_map`, etc.:
- globals()[f"{suffix}_map"] = attention_maps_dict[suffix]
- # Print the shape
- print(f"UNET attention map from layer {suffix}: {attention_maps_dict[suffix].shape}",
- attention_maps_dict[suffix].min(),
- ", max value:",
- attention_maps_dict[suffix].max())
- # Limit to the first loop only (for task2)
- line_oi = complete_cases[(complete_cases["stimulus"] == file_prefix) & (complete_cases["pp_id"] == pp_id)]
- if line_oi["multiple_bodies"].item() == "yes":
- #THEN DEAL WITH THIS
- bodies_oi = body_info[(body_info["stimulus"] == file_prefix) & (body_info["pp_id"] == pp_id)]
- # Here select for each line the times where timestamp >= start_time <= endtime
- # Initialize an empty list to collect all body outline files
- outline_file = pd.DataFrame()
- # Loop through each body and filter task2_mouse accordingly
- for _, body_row in bodies_oi.iterrows():
- # Select times where timestamp >= start_time <= end_time for the body
- body_start_time = body_row["start_time"]
- body_end_time = body_row["end_time"]
- # Filter task2_mouse based on the body's start and end time
- body_outline_file = task2_mouse[["timestamp", "x_shifted", "y_shifted", "velocity"]]
- body_outline_file = body_outline_file[(body_outline_file["timestamp"] >= body_start_time) &
- (body_outline_file["timestamp"] <= body_end_time)]
- # Append the body outline file to the combined dataframe
- outline_file = pd.concat([outline_file, body_outline_file], ignore_index=True)
- else:
- start_time = line_oi["start_time"].item()
- end_time = line_oi["end_time"].item()
- # Start working on a mouse-file
- outline_file = task2_mouse[["timestamp", "x_shifted", "y_shifted", "velocity"]]
- outline_file = outline_file[(outline_file["timestamp"] >= start_time) & (outline_file["timestamp"] <= end_time)]
- # Extract the added fixation duration in that area for task 1
- # Create lists to store results
- fix_count_list = []
- fix_duration_list = []
- avg_saccade_amp_list = []
- avg_saccade_peakvelocity_list = []
- distance_truth_list = []
- # Loop over each point in the outline
- for _, outline_row in outline_file.iterrows():
- x_o = outline_row["x_shifted"]
- y_o = outline_row["y_shifted"]
- # Boolean mask: which fixations fall within a 20 pixel radius
- # Compute distance from center
- dx = task1_fixations["x_shifted"] - x_o
- dy = task1_fixations["y_shifted"] - y_o
- distance = np.sqrt(dx**2 + dy**2)
- # Boolean mask: fixations within radius 20
- nearby_fixations = task1_fixations[distance <= 20]
- # Count and sum duration
- fix_count = len(nearby_fixations)
- fix_duration = nearby_fixations["duration"].sum() if fix_count > 0 else 0
- # Store values
- fix_count_list.append(fix_count)
- fix_duration_list.append(fix_duration)
- # Distance from saccade start to (x_o, y_o)
- start_dist = np.sqrt((task1_saccades["sx_shifted"] - x_o)**2 +
- (task1_saccades["sy_shifted"] - y_o)**2)
- # Distance from saccade end to (x_o, y_o)
- end_dist = np.sqrt((task1_saccades["ex_shifted"] - x_o)**2 +
- (task1_saccades["ey_shifted"] - y_o)**2)
- # Compute distances from (x_o, y_o) to saccade start and end points
- start_dist = np.sqrt((task1_saccades["sx_shifted"] - x_o)**2 +
- (task1_saccades["sy_shifted"] - y_o)**2)
- end_dist = np.sqrt((task1_saccades["ex_shifted"] - x_o)**2 +
- (task1_saccades["ey_shifted"] - y_o)**2)
- # Boolean mask: saccades within 20 pixels (circular region)
- within_radius_mask = (start_dist <= 20) | (end_dist <= 20)
- nearby_saccades = task1_saccades[within_radius_mask]
- if not nearby_saccades.empty:
- # Use these for averages
- avg_amp = nearby_saccades["amplitude"].mean()
- avg_peakvel = nearby_saccades["peak_velocity"].mean()
- else:
- # Use closest point(s) instead
- min_start_dist = start_dist.min()
- min_end_dist = end_dist.min()
- # Boolean mask: saccades where start or end is at the closest distance
- closest_mask = (start_dist == min_start_dist) | (end_dist == min_end_dist)
- closest_saccades = task1_saccades[closest_mask]
- # Compute averages from saccades closest to (x_o, y_o)
- avg_amp = closest_saccades["amplitude"].mean()
- avg_peakvel = closest_saccades["peak_velocity"].mean()
- # Store values
- avg_saccade_amp_list.append(avg_amp)
- avg_saccade_peakvelocity_list.append(avg_peakvel)
- # Calculate the minimal distance to the points of the ground truth outline
- # Compute Euclidean distances to all contour points
- # Create point
- point = np.array([x_o, y_o])
- # Get contour points as (x, y) values
- contour_points = contour_df[["x", "y"]].values
- # Compute Euclidean distances from point to all contour points
- distances = np.linalg.norm(contour_points - point, axis=1)
- # Get the minimal distance
- min_distance = distances.min()
- # Get the minimum distance
- min_distance = distances.min()
- distance_truth_list.append(min_distance)
- # Add results to the outline_file DataFrame
- outline_file["fix_count_task1"] = fix_count_list
- outline_file["fix_duration_task1"] = fix_duration_list
- outline_file["avg_sacc_amplitude_task1"] = avg_saccade_amp_list
- outline_file["avg_sacc_peakvel_task1"] = avg_saccade_peakvelocity_list
- outline_file["distance_to_truth"] = distance_truth_list
- # Extract the added fixation duration in that area for task 2 (CURRENTLY FOR ENTIRE DURATION)
- # Create lists to store results
- fix_count_list = []
- fix_duration_list = []
- avg_saccade_amp_list = []
- avg_saccade_peakvelocity_list = []
- # Loop over each point in the outline
- for _, outline_row in outline_file.iterrows():
- x_o = outline_row["x_shifted"]
- y_o = outline_row["y_shifted"]
- # Boolean mask: which fixations fall within a 20 pixel radius
- # Compute distance from center
- dx = task2_fixations["x_shifted"] - x_o
- dy = task2_fixations["y_shifted"] - y_o
- distance = np.sqrt(dx**2 + dy**2)
- # Boolean mask: fixations within radius 20
- nearby_fixations = task2_fixations[distance <= 20]
- # Count and sum duration
- fix_count = len(nearby_fixations)
- fix_duration = nearby_fixations["duration"].sum() if fix_count > 0 else 0
- # Store values
- fix_count_list.append(fix_count)
- fix_duration_list.append(fix_duration)
- # Distance from saccade start to (x_o, y_o)
- start_dist = np.sqrt((task2_saccades["sx_shifted"] - x_o)**2 +
- (task2_saccades["sy_shifted"] - y_o)**2)
- # Distance from saccade end to (x_o, y_o)
- end_dist = np.sqrt((task2_saccades["ex_shifted"] - x_o)**2 +
- (task2_saccades["ey_shifted"] - y_o)**2)
- # Compute distances from (x_o, y_o) to saccade start and end points
- start_dist = np.sqrt((task2_saccades["sx_shifted"] - x_o)**2 +
- (task2_saccades["sy_shifted"] - y_o)**2)
- end_dist = np.sqrt((task2_saccades["ex_shifted"] - x_o)**2 +
- (task2_saccades["ey_shifted"] - y_o)**2)
- # Boolean mask: saccades within 20 pixels (circular region)
- within_radius_mask = (start_dist <= 20) | (end_dist <= 20)
- nearby_saccades = task2_saccades[within_radius_mask]
- if not nearby_saccades.empty:
- # Use these for averages
- avg_amp = nearby_saccades["amplitude"].mean()
- avg_peakvel = nearby_saccades["peak_velocity"].mean()
- else:
- # Use closest point(s) instead
- min_start_dist = start_dist.min()
- min_end_dist = end_dist.min()
- # Boolean mask: saccades where start or end is at the closest distance
- closest_mask = (start_dist == min_start_dist) | (end_dist == min_end_dist)
- closest_saccades = task2_saccades[closest_mask]
- # Compute averages from saccades closest to (x_o, y_o)
- avg_amp = closest_saccades["amplitude"].mean()
- avg_peakvel = closest_saccades["peak_velocity"].mean()
- # Store values
- avg_saccade_amp_list.append(avg_amp)
- avg_saccade_peakvelocity_list.append(avg_peakvel)
- # Add results to the outline_file DataFrame
- outline_file["fix_count_task2"] = fix_count_list
- outline_file["fix_duration_task2"] = fix_duration_list
- outline_file["avg_sacc_amplitude_task2"] = avg_saccade_amp_list
- outline_file["avg_sacc_peakvel_task2"] = avg_saccade_peakvelocity_list
- # Assess the number of NAs per column (there should not be any):
- column_nan_count = outline_file.isnull().sum()
- print("NaN count per column:")
- print(column_nan_count)
- # ADD information from the maps
- radius = 20
- diameter = radius * 2 + 1
- # Step 1: Create a circular mask
- yy, xx = np.ogrid[-radius:radius+1, -radius:radius+1]
- circle_mask = (xx**2 + yy**2) <= radius**2
- # Prepare output lists
- avg_task1_fixation = []
- avg_task2_fixation = []
- avg_entropy = []
- avg_attention_maps = {name: [] for name in attention_maps_dict} # dict of {name: 2D array}
- # Step 2: Loop over outline points
- for _, row in outline_file.iterrows():
- x_o = int(round(row["x_shifted"]))
- y_o = int(round(row["y_shifted"]))
- # Define bounding box
- x_start, x_end = x_o - radius, x_o + radius + 1
- y_start, y_end = y_o - radius, y_o + radius + 1
- # Helper to extract and mask the region
- def extract_avg(img):
- h, w = img.shape
- if x_start < 0 or y_start < 0 or x_end > w or y_end > h:
- return np.nan # out of bounds
- region = img[y_start:y_end, x_start:x_end]
- return region[circle_mask].mean()
- avg_task1_fixation.append(extract_avg(task1_fixationmap))
- avg_task2_fixation.append(extract_avg(task2_fixationmap))
- avg_entropy.append(extract_avg(entropy_map))
- for name, amap in attention_maps_dict.items():
- avg_attention_maps[name].append(extract_avg(amap))
- # Step 3: Add to DataFrame
- outline_file["avg_task1_fix"] = avg_task1_fixation
- outline_file["avg_task2_fix"] = avg_task2_fixation
- outline_file["avg_entropy"] = avg_entropy
- for name, values in avg_attention_maps.items():
- outline_file[f"avg_{name}"] = values
- # Assess the number of NAs per column (there should not be any):
- column_nan_count = outline_file.isnull().sum()
- print("NaN count per column:")
- print(column_nan_count)
- # Add interobserver variability, consider only the participants with DICE >0.7
- # Before this: 143 pp-stimuli pairs
- # After this: still 94 pp-stimuli pairs
- # 1. Filter loop_info_first for DICE > 0.7
- loop_info_highDICE = loop_info_first[loop_info_first["DICE_truth"] > 0.7]
- # 2. Get participant IDs for same stimulus and different pp_id
- pp_ids_oi = loop_info_highDICE["pp_id"][
- (loop_info_highDICE["stimulus"] == file_prefix) &
- (loop_info_highDICE["pp_id"] != pp_id)
- ]
- # 3. Load all comparison outlines for this stimulus
- other_pp_outlines = []
- for pp in pp_ids_oi:
- file_path = os.path.join(base_path, "csv", f"{file_prefix}_task2_{pp}_mouse_firstloop.csv")
- if os.path.exists(file_path):
- df = pd.read_csv(file_path)
- other_pp_outlines.append(df[["x_shifted", "y_shifted"]].values)
- # 4. Compute variability for each point in the current outline
- variability_list = []
- for _, outline_row in outline_file.iterrows():
- x_o = outline_row["x_shifted"]
- y_o = outline_row["y_shifted"]
- point = np.array([x_o, y_o])
- # Store all minimal distances to this point across other participant outlines
- min_dists = []
- for other_outline in other_pp_outlines:
- # Euclidean distances to all points in this other outline
- dists = np.linalg.norm(other_outline - point, axis=1)
- min_dists.append(dists.min()) # Closest point on this outline
- # Median across all other outlines
- if min_dists:
- variability_list.append(np.median(min_dists))
- else:
- variability_list.append(np.nan)
- # 5. Add to DataFrame
- outline_file["variability_median_min_dist"] = variability_list
- # Select only continuous variables (excluding 'timestamp', 'x_shifted', 'y_shifted')
- continuous_vars = outline_file.drop(columns=['timestamp', 'x_shifted', 'y_shifted'])
- # Step 1: Compute the correlation matrix
- corr_matrix = continuous_vars.corr()
- # Step 2: Compute p-values for correlation significance
- def compute_p_value(x, y):
- return pearsonr(x, y)[1]
- p_values = continuous_vars.corr(method=lambda x, y: compute_p_value(x, y)) # Get p-values
- # Step 3: Handle missing data for saccade columns by excluding rows with NaN in these columns
- # Create a mask for NaN in saccade columns (fixation columns are kept as is)
- mask_saccades = continuous_vars[['avg_sacc_amplitude_task1', 'avg_sacc_peakvel_task1',
- 'avg_sacc_amplitude_task2', 'avg_sacc_peakvel_task2']].notna()
- # Apply mask only to saccade-related columns
- mask = mask_saccades.all(axis=1)
- filtered_continuous_vars = continuous_vars[mask]
- # Recompute correlation matrix and p-values after filtering
- corr_matrix_filtered = filtered_continuous_vars.corr()
- p_values_filtered = filtered_continuous_vars.corr(method=lambda x, y: compute_p_value(x, y))
- # Step 4: Define significance stars function
- def significance_stars(p):
- if p < 0.001:
- return "***"
- elif p < 0.01:
- return "**"
- elif p < 0.05:
- return "*"
- else:
- return ""
- # Step 5: Generate the annotation for the heatmap
- # Format correlation values and add significance stars
- annot = p_values_filtered.applymap(significance_stars)
- #corr_annot = corr_matrix_filtered.round(2).astype(str) + annot
- corr_annot = annot # only significance start
- # Step 6: Plot the heatmap
- plt.figure(figsize=(12, 10))
- sns.heatmap(corr_matrix_filtered, annot=corr_annot, fmt="", cmap="coolwarm", center=0, linewidths=0.5, vmin=-1, vmax=1)
- # Title
- plt.title("Correlation Heatmap with Significance Levels (***: p < 0.001, **: p < 0.01. *: p < 0.05)", fontsize=14)
- plt.show()
- # Plot this information
- plt.scatter(outline_file["fix_count_task1"], outline_file["fix_count_task2"])
- # Add labels and title for clarity
- plt.xlabel('Fixation Count (Task 1)')
- plt.ylabel('Fixation Count (Task 2)')
- # Display the plot
- plt.show()
- # Function to plot saccades, fixations, and mouse track annotations
- def plot_saccades_and_fixations(image, saccades, fixations, title="Plot", output_path=None):
- # Create a figure and axis for the plot
- fig, ax = plt.subplots(figsize=(8, 8))
- ax.imshow(image) # Display the image
- # Plot saccades
- for _, row in saccades.iterrows():
- sx, sy, ex, ey = row['sx_shifted'], row['sy_shifted'], row['ex_shifted'], row['ey_shifted']
- ax.plot([sx, ex], [sy, ey],
- color='gold',
- alpha = 0.5,
- linewidth=1)
- # Plot fixations
- for _, row in fixations.iterrows():
- x, y, duration = row['x_shifted'], row['y_shifted'], row['duration']
- radius = np.sqrt(duration) * 0.5
- circle = plt.Circle((x, y), radius, color='violet', alpha=0.9, fill=False, linewidth=1.5)
- ax.add_patch(circle)
- # Title and axis settings
- ax.set_title(title, fontsize=14)
- ax.axis("off")
- # Save or display the plot
- #if output_path:
- #plt.savefig(output_path, bbox_inches='tight')
- plt.show()
- # Plot for task1
- plot_saccades_and_fixations(
- image=image, # Load your image here
- saccades=task1_saccades, # Load saccades dataframe here
- fixations=task1_fixations, # Load fixations dataframe here
- title="Saccades and Fixations Visualization (task 1)",
- output_path=None # If you want to save, provide the path
- )
- # Plot for task2
- plot_saccades_and_fixations(
- image=image, # Load your image here
- saccades=task2_saccades, # Load saccades dataframe here
- fixations=task2_fixations, # Load fixations dataframe here
- title="Saccades and Fixations Visualization (task2)",
- output_path=None # If you want to save, provide the path
- )
- # Create a truncated version of the magma colormap
- def truncate_colormap(cmap, minval=0.1, maxval=1.0, n=100):
- """
- Truncates a colormap so that it starts at minval and ends at maxval.
- """
- new_cmap = mcolors.LinearSegmentedColormap.from_list(
- f"truncated_{cmap.name}", cmap(np.linspace(minval, maxval, n))
- )
- return new_cmap
- # Use magma colormap and truncate it
- cmap = plt.get_cmap('magma')
- cmap_truncated = truncate_colormap(cmap, minval=0.2) # Adjust minval as needed to start from purple
- # Function to plot mouse track annotations
- def plot_mouse_track_annotations(image, outline_file, annot_column, title="Plot", output_path=None):
- # Create a figure and axis for the plot
- fig, ax = plt.subplots(figsize=(8, 8))
- ax.imshow(image) # Display the image
- # Normalize the annotation column values for coloring
- norm = plt.Normalize(vmin=outline_file[annot_column].min(), vmax=outline_file[annot_column].max())
- cmap = cmap_truncated # You can use other color maps like 'coolwarm', 'plasma', etc.
- # Plot mouse track (annotate by the specified column)
- scatter = ax.scatter(outline_file['x_shifted'], outline_file['y_shifted'],
- c=outline_file[annot_column], cmap=cmap, norm=norm, s=10, alpha=0.7)
- # Add a smaller colorbar to the plot for better understanding of the metric values
- cbar = plt.colorbar(scatter, ax=ax, orientation='vertical', fraction=0.02, pad=0.04) # Adjust fraction and pad
- cbar.set_label(annot_column, fontsize=10)
- # Set the title to include the annotation column
- ax.set_title(f"{title} - Annotated by {annot_column}", fontsize=14)
- # Remove axes
- ax.axis("off")
- # Save or display the plot
- #if output_path:
- #plt.savefig(output_path, bbox_inches='tight')
- plt.show()
- # Function to plot mouse track annotations for all columns in outline_file
- def plot_for_all_columns(image, outline_file, title="Mouse Track with Annotations", output_path=None):
- # Iterate over each column in outline_file (excluding columns like 'x_shifted', 'y_shifted')
- for column in outline_file.columns:
- # Skip the columns that are not suitable for annotation (e.g., 'x_shifted', 'y_shifted', etc.)
- if column in ['x_shifted', 'y_shifted', 'timestamp']: # Adjust as necessary
- continue
- # Call the plot function for each column
- plot_mouse_track_annotations(
- image=image,
- outline_file=outline_file,
- annot_column=column,
- title=f"{title} - Annotated by {column}",
- output_path=output_path # You can provide a path to save the plot
- )
- # Example usage
- plot_for_all_columns(
- image=image, # Load your image here
- outline_file=outline_file, # Load outline_file dataframe here
- title="Mouse Track with Annotations", # Title for the plot
- output_path=None # If you want to save, provide the path
- )
annotate_mouse_track.py at commit 4bd8e5f, no license · at the source
Overview
- ETH Zurich, Rämistrasse101, 8092 Zürich, Switzerland
- Swiss Institute of Bioinformatics, Lagerstrasse 5, Lausanne, Switzerland
- Fachhochschule Nordwestschweiz FHNW, Hochschule für Informatik, 5210 Windisch, Switzerland
- Department of Radiation Oncology, Inselspital, Bern University Hospital and University of Bern, Switzerland
- Department of Digital Medicine, University of Bern, Bern, Switzerland
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
4bd8e5f9b43bf98c89833865c088dc2254f99204, 29 May 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
34 files
- preprocessing/
event_trial_filtering_di , Python, 56 linesagram.py - preprocessing/
stretch.py , Python, 43 lines - preprocessing/
utils/ , Python, 194 linesDICE_HD_calculation.py - preprocessing/
utils/ , Python, 190 linesGet_individual_files.py - preprocessing/
utils/ , Python, 252 linesPlot_eye_mouse.py - preprocessing/
utils/ , Python, 252 linesPlot_eye_mouse_streched. py - preprocessing/
utils/ , Python, 104 linesdisconnected_bodies.py - preprocessing/
utils/ , Python, 64 linesdispersion_threshold.py - preprocessing/
utils/ , Python, 123 linesentropy_calculation.py - preprocessing/
utils/ , Python, 97 linesfixmap_autothresholding. py - preprocessing/
utils/ , Python, 77 linesfixmap_differences.py - preprocessing/
utils/ , Python, 114 linesheatmap_pygaze_allpp_all stimuli_alltasks.py - preprocessing/
utils/ , Python, 96 linesmouse_loop_extraction.py - preprocessing/
utils/ , Python, 92 linesresize_attention_maps.py - preprocessing/
utils/ , Python, 46 linesresize_masks.py - preprocessing/
utils/ , Python, 53 linessum_attention_maps.py - preprocessing/
utils/ , Python, 101 linesvariability_annotation.p y - uncertainty_models/
assess_unet_uncertainty. , Python, 477 lines, 1 matchpy - uncertainty_models/
classification/ , Python, 339 lines, 1 matchbinary_model.py - uncertainty_models/
classification/ , Shell, 29 linesrun_binarymodel.sh - uncertainty_models/
linear_mixed_effects/ , R, 224 lines, 1 matchmodel_LMR.Rmd - uncertainty_models/
regression/ , Python, 235 lines, 1 matchbest_model.py - uncertainty_models/
regression/ , Shell, 29 linesrun_best_model.sh - uncertainty_models/
spatial_human_model_unce , Python, 250 linesrtainty.py - uncertainty_models/
utils/ , Python, 722 lines, 1 matchannotate_mouse_track.py - uncertainty_models/
utils/ , Python, 735 lines, 1 matchannotate_mouse_track_HDw eighting.py - uncertainty_models/
utils/ , Python, 364 linesannotate_mouse_tracks_al lpp_allstimuli.py - unet/
guided_diffusion/ , Python, 161 linesunet_seg.py - unet/
scripts/ , Python, 62 linesgrad_cam.py - unet/
scripts/ , Shell, 36 linesrun_seg_brats.sh - unet/
scripts/ , Python, 309 linessegmentation.py - unet/
scripts/ , Python, 248 linessegmentation_test.py - unet/
scripts/ , Python, 340 lines, 1 matchsegmentation_test_seg_pr ob_atten_alllayers.py - README.md, Text, 119 lines
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
- synapse.org/
synapse:syn51156910 , at Synapse; found in “Data sharing”
Data sharing
Imaging data is publicly available at https://
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://
BibTeX
@article{baumgartner2026
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/
url = {https://
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/
VL - 40
SP - 101056
SN - 2405-6316
PB - Elsevier
DO - 10.1016/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1016/
"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":
"volume": "40",
"page": "101056",
"DOI": "10.1016/
"PMID": "42630840",
"PMCID": "PMC13495590",
"ISSN": "2405-6316",
"publisher": "Elsevier",
"URL": "https://
"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 biologyIn 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 reportsIn 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. MedicineIn 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. ClinicalIn 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 journalIn 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 communicationsIn 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 communicationsIn 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 communicationsIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 33 scripts, and 7 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:c7c2ca1ba210a96f…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[.
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.
