R2-V2 / infer.py
j-morano's picture
Add bv model (safetensors) with standalone inference pipeline
bb8c6c9
Raw
History Blame Contribute Delete
12.3 kB
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
########################################################################
# Dataclasses
@dataclass
class DataPaths:
cfp: Path
pre: Path | None
mask: Path | None
@dataclass
class ResizedImage:
img: NDArray
ohw: tuple[int, int] | None # original height and width
########################################################################
# Functions
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 = []
# Rotate 90, 180, 270 degrees, and flip horizontally and vertically
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)
# Combine predictions from all augmentations by majority voting
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)
# print(f' bv_av shape: {bv_av.shape}')
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]
# a_uni[a_uni <= 0.5] = a.min(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]
# v_uni[v_uni <= 0.5] = v.min(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]
# bv_uni[bv_uni <= 0.5] = bv.min(dim=1, keepdim=True).values[bv_uni <= 0.5]
# elif option == 1:
# a_uni = torch.median(a, dim=1, keepdim=True).values
# v_uni = torch.median(v, dim=1, keepdim=True).values
# bv_uni = torch.median(bv, dim=1, keepdim=True).values
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)
# Assign the maximum value to the artery channel, if the mean
# value is greater than 0.5, otherwise assign 1 - value
# a_uni = torch.where(a_uni > 0.5, a.max(dim=0, keepdim=True)[0], 1 - a.min(dim=0, keepdim=True)[0])
else:
# option = 1
# if option == 0:
# a_uni = all_preds[0][:, 0:1]
# v_uni = all_preds[0][:, 1:2]
# bv_uni = all_preds[0][:, 2:3]
# # print(f' {a_uni.shape}, {v_uni.shape}, {bv_uni.shape}')
# bv_uni = torch.max(torch.stack([bv_uni, a_uni, v_uni], dim=1), dim=1).values
# 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
########################################################################
# Functions
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))
# Namespace from config dict
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]
# padding format: ((top, bottom), (left, right), (0, 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()