| from pathlib import Path |
| import argparse |
| from argparse import Namespace |
| import json |
| import gc |
| from dataclasses import dataclass |
|
|
| import numpy as np |
| from numpy.typing import NDArray |
| import torch |
| from torch.nn import functional as F |
| from torch import Tensor |
| from torch import nn |
| from torchvision import utils as vutils |
| from skimage import io |
| from skimage.transform import resize |
|
|
| from transformations import to_torch_tensors, pad_images_unet |
| from model import RRWNet |
| import preprocessing |
|
|
|
|
|
|
| |
| |
|
|
|
|
| @dataclass |
| class DataPaths: |
| cfp: Path |
| pre: Path | None |
| mask: Path | None |
|
|
|
|
| @dataclass |
| class ResizedImage: |
| img: NDArray |
| ohw: tuple[int, int] | None |
|
|
|
|
| |
| |
|
|
|
|
| def get_args() -> Namespace: |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '-i', '--cfp_path', |
| type=str, required=True, |
| help='Path to input CFP images (required)' |
| ) |
| parser.add_argument( |
| '-t', '--model_type', |
| type=str, choices=['av', 'bv'], default='av', |
| help='Type of model to use: av (artery/vein) or bv (blood vessels) [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '-w', '--weights_path', |
| type=str, default='./__weights', |
| help='Path to weights directory [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '--weights', |
| type=str, default=None, |
| help='Path to weights file (overrides model_type and weights_path) [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '-s', '--save_path', |
| type=str, default='./__predictions', |
| help='Path to save predictions [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '-m', '--masks_path', |
| type=str, default=None, |
| help='Path to the masks [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '-p', '--pre_path', |
| type=str, default=None, |
| help='Path to the preprocessed images [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '-g', '--use-gave-format', |
| action='store_true', default=False, |
| help='Save output in GAVE format (R for arteries, B for all' |
| ' blood vessels, G for veins); otherwise the format is' |
| ' as in RRWNet (R for arteries, G for veins, B for all' |
| ' blood vessels) [default: %(default)s]' |
| ) |
| parser.add_argument( |
| '--tta', |
| action='store_true', default=False, |
| help='Use test time augmentation [default: %(default)s]' |
| ) |
| return parser.parse_args() |
|
|
|
|
| def get_models_prediction(model: nn.Module, tensor: Tensor) -> Tensor: |
| pred = model(tensor) |
| if isinstance(pred, list): |
| pred = pred[-1] |
| return pred |
|
|
|
|
| def get_predictions( |
| model: nn.Module, |
| tensor: Tensor, |
| mask_tensor: Tensor, |
| model_type: str, |
| test_aug: bool = True, |
| ) -> Tensor: |
| original_tensor = tensor.clone() |
| if test_aug: |
| print(' Using test time augmentation: rotations and flips') |
| tensors = [] |
| |
| for angle in [0, 90, 180, 270]: |
| rotated_tensor = torch.rot90(original_tensor, k=angle // 90, dims=(2, 3)) |
| for flip in [False, True]: |
| if flip: |
| flipped_tensor = torch.flip(rotated_tensor, dims=(3,)) |
| else: |
| flipped_tensor = rotated_tensor.clone() |
| tensors.append((flipped_tensor, angle, flip)) |
| else: |
| tensors = [(tensor, 0, False)] |
| all_preds = [] |
| for tensor, angle, flip in tensors: |
| pred = get_models_prediction(model, tensor) |
| pred = torch.sigmoid(pred) |
| if flip: |
| pred = torch.flip(pred, dims=(3,)) |
| if angle > 0: |
| pred = torch.rot90(pred, k=-angle // 90, dims=(2, 3)) |
| pred[mask_tensor < 0.5] = 0 |
| all_preds.append(pred) |
| |
| if len(all_preds) > 1: |
| print(f' Combining {len(all_preds)} predictions') |
| a = torch.stack([x[:, 0] for x in all_preds], dim=1) |
| v = torch.stack([x[:, 1] for x in all_preds], dim=1) |
| bv = torch.stack([x[:, 2] for x in all_preds], dim=1) |
| if model_type == 'bv': |
| print(' Using bv specific combination') |
| a_uni = torch.mean(a, dim=1, keepdim=True) |
| v_uni = torch.mean(v, dim=1, keepdim=True) |
| av = (a + v).clamp(min=0, max=1) |
| bv_av = torch.concatenate([bv, av], dim=1) |
| |
| bv_uni = torch.mean(bv_av, dim=1, keepdim=True) |
| a_uni[a_uni > 0.5] = a.max(dim=1, keepdim=True).values[a_uni > 0.5] |
| |
| v_uni[v_uni > 0.5] = v.max(dim=1, keepdim=True).values[v_uni > 0.5] |
| |
| bv_uni[bv_uni > 0.5] = bv_av.max(dim=1, keepdim=True).values[bv_uni > 0.5] |
| |
| |
| |
| |
| |
| else: |
| print(' Using simple mean combination') |
| a_uni = torch.mean(a, dim=1, keepdim=True) |
| v_uni = torch.mean(v, dim=1, keepdim=True) |
| bv_uni = torch.mean(bv, dim=1, keepdim=True) |
| pred_uni = torch.cat([a_uni, v_uni, bv_uni], dim=1) |
| |
| |
| |
| else: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pred_uni = all_preds[0] |
| return pred_uni |
|
|
|
|
| def in_parent_or_none(ref_path: Path, this_path: str | None, default: str) -> Path | None: |
| if this_path is None: |
| path = ref_path.parent / default |
| else: |
| if Path(this_path).exists(): |
| path = Path(this_path) |
| elif (ref_path.parent / this_path).exists(): |
| path = ref_path.parent / this_path |
| else: |
| path = None |
| return path |
|
|
|
|
| def get_paths(args: Namespace) -> DataPaths: |
| cfp_path = Path(args.cfp_path) |
| assert cfp_path.exists(), cfp_path |
| masks_path = in_parent_or_none(cfp_path, args.masks_path, 'mask') |
| pre_path = in_parent_or_none(cfp_path, args.pre_path, 'pre') |
| return DataPaths( |
| cfp=cfp_path, |
| pre=pre_path, |
| mask=masks_path, |
| ) |
|
|
|
|
|
|
| def get_model(config: Namespace, weights_file: Path, device) -> nn.Module: |
| print('Creating model') |
| model = RRWNet( |
| config.in_channels, |
| config.out_channels, |
| config.base_channels, |
| config.num_iterations |
| ) |
|
|
| print('Loading weights') |
| if weights_file.suffix == '.safetensors': |
| from safetensors.torch import load_model |
| load_model(model, str(weights_file)) |
| else: |
| model.load_state_dict(torch.load(weights_file)) |
|
|
| model.eval() |
|
|
| model.to(device) |
|
|
| return model |
|
|
|
|
| def read_to_zero_one(img_fn) -> ResizedImage: |
| img = io.imread(img_fn).astype(np.float32) |
| if img.max() > 255: |
| img = img / 65535.0 |
| if img.max() > 1: |
| img = img / 255.0 |
| img = np.clip(img, 0.0, 1.0) |
| if len(img.shape) == 3: |
| img = img[..., :3] |
| h, w = img.shape[0], img.shape[1] |
| original_spatial_shape = (h, w) |
| if w != 1408: |
| new_w = 1408 |
| new_h = int(h * (new_w / w)) |
| img = resize(img, (new_h, new_w), anti_aliasing=True, preserve_range=True) |
| else: |
| original_spatial_shape = None |
| if not isinstance(img, np.ndarray): |
| img = np.array(img) |
| if img.dtype != np.float32: |
| img = img.astype(np.float32) |
| return ResizedImage(img, original_spatial_shape) |
|
|
|
|
| def get_corr_fn(ref_fn: Path, path: Path | None) -> Path | None: |
| corr_fn = None |
| if path is not None and path.exists(): |
| for corr_fn in path.iterdir(): |
| if corr_fn.stem == ref_fn.stem: |
| break |
| return corr_fn |
|
|
|
|
|
|
| |
| |
|
|
|
|
| def main(): |
| args = get_args() |
| use_cfp = args.model_type == 'bv' |
| test_aug = args.tta |
|
|
| save_path = Path(args.save_path) / args.model_type |
| save_path.mkdir(exist_ok=True, parents=True) |
|
|
| paths = get_paths(args) |
| print('Paths:', paths) |
| weights_path = Path(args.weights_path) |
|
|
| print(f'Loading model: {args.model_type}') |
| if args.weights is not None: |
| print(' Overriding default weights, using', args.weights) |
| weights_file = Path(args.weights) |
| else: |
| weights_file = weights_path / f'{args.model_type}.safetensors' |
|
|
| print('Loading config') |
| with open(weights_path / f'{args.model_type}_config.json', 'r') as f: |
| config = json.load(f) |
|
|
| print('Config:') |
| print(json.dumps(config, indent=4)) |
|
|
| |
| config = argparse.Namespace(**config) |
|
|
| device = 'cuda' if torch.cuda.is_available() else 'cpu' |
|
|
| model = get_model(config, weights_file, device) |
|
|
| for cfp_fn in sorted(paths.cfp.iterdir()): |
| if not cfp_fn.is_file(): |
| continue |
| mask_fn = get_corr_fn(cfp_fn, paths.mask) |
| pre_fn = get_corr_fn(cfp_fn, paths.pre) |
| print(f'> Processing {cfp_fn.name}') |
| cfp_res = read_to_zero_one(cfp_fn) |
| cfp = cfp_res.img |
| print(f' CFP shape: {cfp.shape}') |
| if mask_fn is None: |
| print(f' Warning: No mask found, using thresholding') |
| mask = (cfp.sum(axis=2) > 0.01).astype(np.float32) |
| else: |
| mask = read_to_zero_one(mask_fn).img.astype(np.float32) |
| if pre_fn is not None: |
| pre = read_to_zero_one(pre_fn).img |
| else: |
| print(f' Warning: No preprocessed image found, preprocessing on the fly') |
| pre, mask = preprocessing.preprocess_img(cfp, mask) |
| print(f' Preprocessed shape: {pre.shape}') |
| if use_cfp: |
| img = np.concatenate([pre, cfp], axis=-1) |
| else: |
| img = pre |
| images, paddings = pad_images_unet([img, mask], return_paddings=True) |
| img = images[0] |
| mask = images[1] |
| padding = paddings[0] |
| |
| with torch.no_grad(): |
| tensors = to_torch_tensors([img, mask], device=device) |
| tensor = tensors[0] |
| mask_tensor = tensors[1] |
| pred = get_predictions( |
| model, |
| tensor, |
| mask_tensor, |
| model_type=args.model_type, |
| test_aug=test_aug, |
| ) |
| pred = pred[..., padding[0][0]:-padding[0][1], padding[1][0]:-padding[1][1]] |
| if args.use_gave_format: |
| pred_form = torch.zeros_like(pred) |
| pred_form[:, 0] = pred[:, 0] |
| pred_form[:, 1] = pred[:, 2] |
| pred_form[:, 2] = pred[:, 1] |
| else: |
| pred_form = pred |
| if cfp_res.ohw is not None: |
| pred_form = F.interpolate( |
| pred_form, |
| size=cfp_res.ohw, |
| mode='bilinear', |
| align_corners=False |
| ) |
| save_fn = save_path / Path(cfp_fn).name |
| vutils.save_image(pred_form, save_fn) |
| gc.collect() |
| torch.cuda.empty_cache() |
|
|
| print('Images saved in', save_path) |
|
|
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|