Source code for ClearMap.ImageProcessing.machine_learning.vertices_classification.degree1_inference_utils

"""
This module provides functionality to run inference on 3D patches extracted from a 3D image,

The main user functions are `run_inference`, which extracts patches centered on degree=1 graph vertices,
and `update_graph_properties`, which updates the graph with new properties based on the inference results.
"""
from pathlib import Path

import numpy as np
import pandas as pd

import torch
import torch.nn as nn
import torch.nn.functional as Fnn
from torch.utils.data import Dataset, DataLoader
from torchvision.models.video import swin3d_s


MAX_WORKERS_LOADER = 8


[docs] def get_default_model_path(weights_file_name="swin3d_s_weights.pth"): """ Get the default path to the pre-trained Swin3D model weights. Parameters ---------- weights_file_name : str Name of the weights file. Default is "swin3d_s_weights.pth". Returns ------- str Path to the pre-trained model weights. """ return Path(__file__).parent / "weights" / weights_file_name
[docs] class Swin3DModel(nn.Module): """ Swin3D-based binary classifier for degree=1 tip cells and interrupted vessels. """ def __init__(self, num_classes=2): super().__init__() self.model = swin3d_s(weights=None) in_features = self.model.head.in_features self.model.head = nn.Sequential( nn.Dropout(p=0.3), nn.Linear(in_features, num_classes) )
[docs] def forward(self, x): return self.model(x)
[docs] def load_model(weights_path): """ Load the Swin3D model with pre-trained weights. Parameters ---------- weights_path: str or Path Path to the model weights file. Returns ------- Swin3DModel An instance of the Swin3DModel with loaded weights. """ model = Swin3DModel(num_classes=2).cuda() state_dict = torch.load(weights_path, map_location="cuda", weights_only=True) model.load_state_dict(state_dict, strict=True) model.eval() return model
[docs] class PatchDataset(Dataset): """ Dataset for extracting 3D patches centered on degree=1 graph vertices. """ def __init__(self, vertices_df, image, shape=(30, 30, 30)): self.image = image self.dimensions = np.array(shape) self.half_dim = self.dimensions // 2 self.image_shape = np.array(image.shape) self.vertices_df = vertices_df.copy() # deepcopy not needed ? self.vertices_df = self.vertices_df[self.vertices_df["degree"] == 1].reset_index(drop=True) self.filter_valid_vertices() def __len__(self): return len(self.vertices_df) def __getitem__(self, idx): vertex = self.vertices_df.iloc[idx] vertex_id = int(vertex["vertex_id"]) x, y, z = map(int, (vertex["x"], vertex["y"], vertex["z"])) x_min, x_max = x - self.half_dim[0], x + self.half_dim[0] y_min, y_max = y - self.half_dim[1], y + self.half_dim[1] z_min, z_max = z - self.half_dim[2], z + self.half_dim[2] patch = self.image[x_min:x_max, y_min:y_max, z_min:z_max] patch = torch.tensor(patch, dtype=torch.float32).unsqueeze(0).repeat(3, 1, 1, 1) return vertex_id, patch
[docs] def filter_valid_vertices(self): valid_mask = ( (self.vertices_df["x"] - self.half_dim[0] >= 0) & (self.vertices_df["x"] + self.half_dim[0] < self.image_shape[0]) & (self.vertices_df["y"] - self.half_dim[1] >= 0) & (self.vertices_df["y"] + self.half_dim[1] < self.image_shape[1]) & (self.vertices_df["z"] - self.half_dim[2] >= 0) & (self.vertices_df["z"] + self.half_dim[2] < self.image_shape[2]) ) self.vertices_df = self.vertices_df[valid_mask].reset_index(drop=True)
[docs] def run_inference(image, vertices_df, model_weights, patch_shape=(30, 30, 30), batch_size=16): """ Run inference on patches centered on degree=1 graph vertices. Parameters ---------- image : np.ndarray The 3D image from which to extract patches. vertices_df : pd.DataFrame DataFrame containing vertex id, coordinates and degrees. model_weights : str or Path Path to the model weights file. patch_shape : tuple Shape of the patches to extract around each vertex. batch_size : int Batch size for inference. """ model = load_model(model_weights) dataset = PatchDataset(vertices_df, image, shape=patch_shape) dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=MAX_WORKERS_LOADER) all_ids, all_scores = [], [] with torch.no_grad(): for ids, patches in dataloader: patches = patches.cuda() probs = Fnn.softmax(model(patches), dim=1)[:, 1] all_ids.extend(ids.cpu().numpy()) all_scores.extend(probs.cpu().numpy()) return pd.DataFrame({"vertex_id": all_ids, "tip_cell_score": all_scores})
[docs] def update_graph_properties(graph, scores_df, output_graph_path, threshold=0.8): """ Updating the graph with two new properties: tip_cell_score and tip_cell_prediction Parameters ---------- graph : ClearMap.Analysis.Graphs.GraphGt The graph to update with new properties. scores_df : pd.DataFrame DataFrame containing vertex IDs and their corresponding tip cell scores. # FIXME: check other columns output_graph_path: str or Path Path to save the updated graph with new properties. threshold : float Threshold for classifying a vertex as a tip cell. Default is 0.8. """ vertex_degrees = np.array(graph.vertex_degrees()) total_vertices = len(vertex_degrees) mapped_scores = np.full(total_vertices, -1.0, dtype=np.float32) predictions = np.zeros(total_vertices, dtype=np.int32) scores_df = scores_df.set_index("vertex_id") valid_ids = scores_df.index.values mapped_scores[valid_ids] = scores_df.tip_cell_score.values predictions[valid_ids] = (scores_df.tip_cell_score.values > threshold).astype(int) graph.add_vertex_property("tip_cell_score", mapped_scores) graph.add_vertex_property("tip_cell_prediction", predictions) graph.save(output_graph_path) print(f"Updated graph saved to: {output_graph_path}")