philippendres's picture
Upload folder using huggingface_hub
907462b verified
Raw
History Blame Contribute Delete
21.3 kB
# -*- coding: utf-8 -*-
# CellViT networks and adaptions, with shared encoders
#
# UNETR paper and code: https://github.com/tamasino52/UNETR
# SAM paper and code: https://segment-anything.com/
#
# @ Fabian Hörst, fabian.hoerst@uk-essen.de
# Institute for Artifical Intelligence in Medicine,
# University Medicine Essen
from collections import OrderedDict
from functools import partial
from pathlib import Path
from typing import List, Literal, Union
import torch
import torch.nn as nn
from .cellvit import CellViT
from .utils import Conv2DBlock, Deconv2DBlock, ViTCellViT, ViTCellViTDeit
class CellViTShared(CellViT, nn.Module):
"""CellViT Modell for cell segmentation. U-Net like network with vision transformer as backbone encoder
All heads are shared, just final layers are not shared
The modell is having multiple branches:
* tissue_types: Tissue prediction based on global class token
* nuclei_binary_map: Binary nuclei prediction
* hv_map: HV-prediction to separate isolated instances
* nuclei_type_map: Nuclei instance-prediction
* [Optional, if regression loss]:
* regression_map: Regression map for binary prediction
Args:
num_nuclei_classes (int): Number of nuclei classes (including background)
num_tissue_classes (int): Number of tissue classes
embed_dim (int): Embedding dimension of backbone ViT
input_channels (int): Number of input channels
depth (int): Depth of the backbone ViT
num_heads (int): Number of heads of the backbone ViT
extract_layers: (List[int]): List of Transformer Blocks whose outputs should be returned in addition to the tokens. First blocks starts with 1, and maximum is N=depth.
Is used for skip connections. At least 4 skip connections needs to be returned.
mlp_ratio (float, optional): MLP ratio for hidden MLP dimension of backbone ViT. Defaults to 4.
qkv_bias (bool, optional): If bias should be used for query (q), key (k), and value (v) in backbone ViT. Defaults to True.
drop_rate (float, optional): Dropout in MLP. Defaults to 0.
attn_drop_rate (float, optional): Dropout for attention layer in backbone ViT. Defaults to 0.
drop_path_rate (float, optional): Dropout for skip connection . Defaults to 0.
regression_loss (bool, optional): Use regressive loss for predicting vector components.
Adds two additional channels to the binary decoder, but returns it as own entry in dict. Defaults to False.
"""
def __init__(
self,
num_nuclei_classes: int,
num_tissue_classes: int,
embed_dim: int,
input_channels: int,
depth: int,
num_heads: int,
extract_layers: List,
mlp_ratio: float = 4,
qkv_bias: bool = True,
drop_rate: float = 0,
attn_drop_rate: float = 0,
drop_path_rate: float = 0,
regression_loss: bool = False,
):
# For simplicity, we will assume that extract layers must have a length of 4
nn.Module.__init__(self)
assert len(extract_layers) == 4, "Please provide 4 layers for skip connections"
self.patch_size = 16
self.num_tissue_classes = num_tissue_classes
self.num_nuclei_classes = num_nuclei_classes
self.embed_dim = embed_dim
self.input_channels = input_channels
self.depth = depth
self.num_heads = num_heads
self.mlp_ratio = mlp_ratio
self.qkv_bias = qkv_bias
self.extract_layers = extract_layers
self.drop_rate = drop_rate
self.attn_drop_rate = attn_drop_rate
self.drop_path_rate = drop_path_rate
self.regression_loss = regression_loss
self.encoder = ViTCellViT(
patch_size=self.patch_size,
num_classes=self.num_tissue_classes,
embed_dim=self.embed_dim,
depth=self.depth,
num_heads=self.num_heads,
mlp_ratio=self.mlp_ratio,
qkv_bias=self.qkv_bias,
norm_layer=partial(nn.LayerNorm, eps=1e-6),
extract_layers=self.extract_layers,
drop_rate=drop_rate,
attn_drop_rate=attn_drop_rate,
drop_path_rate=drop_path_rate,
)
if self.embed_dim < 512:
self.skip_dim_11 = 256
self.skip_dim_12 = 128
self.bottleneck_dim = 312
else:
self.skip_dim_11 = 512
self.skip_dim_12 = 256
self.bottleneck_dim = 512
offset_branches = 0
if self.regression_loss:
offset_branches = 2
self.branches_output = {
"nuclei_binary_map": 2 + offset_branches,
"hv_map": 2,
"nuclei_type_maps": self.num_nuclei_classes,
}
self.decoder = self.create_upsampling_branch()
self.nuclei_binary_map_decoder = nn.Conv2d(
in_channels=64,
out_channels=2 + offset_branches,
kernel_size=1,
stride=1,
padding=0,
)
self.hv_map_decoder = nn.Conv2d(
in_channels=64,
out_channels=2,
kernel_size=1,
stride=1,
padding=0,
)
self.nuclei_type_maps_decoder = nn.Conv2d(
in_channels=64,
out_channels=self.num_nuclei_classes,
kernel_size=1,
stride=1,
padding=0,
)
def forward(self, x: torch.Tensor, retrieve_tokens: bool = False) -> dict:
"""Forward pass
Args:
x (torch.Tensor): Images in BCHW style
retrieve_tokens (bool, optional): If tokens of ViT should be returned as well. Defaults to False.
Returns:
dict: Output for all branches:
* tissue_types: Raw tissue type prediction. Shape: (B, num_tissue_classes)
* nuclei_binary_map: Raw binary cell segmentation predictions. Shape: (B, 2, H, W)
* hv_map: Binary HV Map predictions. Shape: (B, 2, H, W)
* nuclei_type_map: Raw binary nuclei type preditcions. Shape: (B, num_nuclei_classes, H, W)
* (optional) tokens
* (optional) regression_map
"""
assert (
x.shape[-2] % self.patch_size == 0
), "Img must have a shape of that is divisible by patch_size (token_size)"
assert (
x.shape[-1] % self.patch_size == 0
), "Img must have a shape of that is divisible by patch_size (token_size)"
out_dict = {}
classifier_logits, _, z = self.encoder(x)
out_dict["tissue_types"] = classifier_logits
z0, z1, z2, z3, z4 = x, *z
# performing reshape for the convolutional layers and upsampling (restore spatial dimension)
patch_dim = [int(d / self.patch_size) for d in [x.shape[-2], x.shape[-1]]]
z4 = z4[:, 1:, :].transpose(-1, -2).view(-1, self.embed_dim, *patch_dim)
z3 = z3[:, 1:, :].transpose(-1, -2).view(-1, self.embed_dim, *patch_dim)
z2 = z2[:, 1:, :].transpose(-1, -2).view(-1, self.embed_dim, *patch_dim)
z1 = z1[:, 1:, :].transpose(-1, -2).view(-1, self.embed_dim, *patch_dim)
upsampled = self._forward_upsample(z0, z1, z2, z3, z4, self.decoder)
if self.regression_loss:
nb_map = self.nuclei_binary_map_decoder(upsampled)
out_dict["nuclei_binary_map"] = nb_map[:, :2, :, :]
out_dict["regression_map"] = nb_map[:, 2:, :, :]
else:
out_dict["nuclei_binary_map"] = self.nuclei_binary_map_decoder(upsampled)
out_dict["hv_map"] = self.hv_map_decoder(upsampled)
out_dict["nuclei_type_map"] = self.nuclei_type_maps_decoder(upsampled)
if retrieve_tokens:
out_dict["tokens"] = z4
return out_dict
def _forward_upsample(
self,
z0: torch.Tensor,
z1: torch.Tensor,
z2: torch.Tensor,
z3: torch.Tensor,
z4: torch.Tensor,
branch_decoder: nn.Sequential,
) -> torch.Tensor:
"""Forward upsample branch
Args:
z0 (torch.Tensor): Highest skip
z1 (torch.Tensor): 1. Skip
z2 (torch.Tensor): 2. Skip
z3 (torch.Tensor): 3. Skip
z4 (torch.Tensor): Bottleneck
branch_decoder (nn.Sequential): Branch decoder network
Returns:
torch.Tensor: Branch Output
"""
b4 = branch_decoder.bottleneck_upsampler(z4)
b3 = branch_decoder.decoder3_skip(z3)
b3 = branch_decoder.decoder3_upsampler(torch.cat([b3, b4], dim=1))
b2 = branch_decoder.decoder2_skip(z2)
b2 = branch_decoder.decoder2_upsampler(torch.cat([b2, b3], dim=1))
b1 = branch_decoder.decoder1_skip(z1)
b1 = branch_decoder.decoder1_upsampler(torch.cat([b1, b2], dim=1))
b0 = branch_decoder.decoder0_skip(z0)
b_final = branch_decoder.decoder0_header(torch.cat([b0, b1], dim=1))
return b_final
def create_upsampling_branch(self) -> nn.Module:
"""Create Upsampling branch
Returns:
nn.Module: Upsampling path
"""
# Skip connections
decoder0_skip = nn.Sequential(
Conv2DBlock(3, 32, 3, self.drop_rate),
Conv2DBlock(32, 64, 3, self.drop_rate),
) # skip connection after positional encoding, shape should be H, W, 64
decoder1_skip = nn.Sequential(
Deconv2DBlock(self.embed_dim, self.skip_dim_11, dropout=self.drop_rate),
Deconv2DBlock(self.skip_dim_11, self.skip_dim_12, dropout=self.drop_rate),
Deconv2DBlock(self.skip_dim_12, 128, dropout=self.drop_rate),
) # skip connection 1
decoder2_skip = nn.Sequential(
Deconv2DBlock(self.embed_dim, self.skip_dim_11, dropout=self.drop_rate),
Deconv2DBlock(self.skip_dim_11, 256, dropout=self.drop_rate),
) # skip connection 2
decoder3_skip = nn.Sequential(
Deconv2DBlock(self.embed_dim, self.bottleneck_dim, dropout=self.drop_rate)
) # skip connection 3
# Upsampling
bottleneck_upsampler = nn.ConvTranspose2d(
in_channels=self.embed_dim,
out_channels=self.bottleneck_dim,
kernel_size=2,
stride=2,
padding=0,
output_padding=0,
)
decoder3_upsampler = nn.Sequential(
Conv2DBlock(
self.bottleneck_dim * 2, self.bottleneck_dim, dropout=self.drop_rate
),
Conv2DBlock(
self.bottleneck_dim, self.bottleneck_dim, dropout=self.drop_rate
),
Conv2DBlock(
self.bottleneck_dim, self.bottleneck_dim, dropout=self.drop_rate
),
nn.ConvTranspose2d(
in_channels=self.bottleneck_dim,
out_channels=256,
kernel_size=2,
stride=2,
padding=0,
output_padding=0,
),
)
decoder2_upsampler = nn.Sequential(
Conv2DBlock(256 * 2, 256, dropout=self.drop_rate),
Conv2DBlock(256, 256, dropout=self.drop_rate),
nn.ConvTranspose2d(
in_channels=256,
out_channels=128,
kernel_size=2,
stride=2,
padding=0,
output_padding=0,
),
)
decoder1_upsampler = nn.Sequential(
Conv2DBlock(128 * 2, 128, dropout=self.drop_rate),
Conv2DBlock(128, 128, dropout=self.drop_rate),
nn.ConvTranspose2d(
in_channels=128,
out_channels=64,
kernel_size=2,
stride=2,
padding=0,
output_padding=0,
),
)
decoder0_header = nn.Sequential(
Conv2DBlock(64 * 2, 64, dropout=self.drop_rate),
Conv2DBlock(64, 64, dropout=self.drop_rate),
)
decoder = nn.Sequential(
OrderedDict(
[
("decoder0_skip", decoder0_skip),
("decoder1_skip", decoder1_skip),
("decoder2_skip", decoder2_skip),
("decoder3_skip", decoder3_skip),
("bottleneck_upsampler", bottleneck_upsampler),
("decoder3_upsampler", decoder3_upsampler),
("decoder2_upsampler", decoder2_upsampler),
("decoder1_upsampler", decoder1_upsampler),
("decoder0_header", decoder0_header),
]
)
)
return decoder
class CellViT256Shared(CellViTShared):
"""CellViT with ViT-256 backbone settings (https://github.com/mahmoodlab/HIPT/blob/master/HIPT_4K/Checkpoints/vit256_small_dino.pth)
All heads are shared, just final layers are not shared
Args:
model256_path (Union[Path, str]): Path to ViT 256 backbone model
num_nuclei_classes (int): Number of nuclei classes (including background)
num_tissue_classes (int): Number of tissue classes
drop_rate (float, optional): Dropout in MLP. Defaults to 0.
attn_drop_rate (float, optional): Dropout for attention layer in backbone ViT. Defaults to 0.
drop_path_rate (float, optional): Dropout for skip connection . Defaults to 0.
regression_loss (bool, optional): Use regressive loss for predicting vector components.
Adds two additional channels to the binary decoder, but returns it as own entry in dict. Defaults to False.
"""
def __init__(
self,
model256_path: Union[Path, str],
num_nuclei_classes: int,
num_tissue_classes: int,
drop_rate: float = 0,
attn_drop_rate: float = 0,
drop_path_rate: float = 0,
regression_loss: bool = False,
):
self.patch_size = 16
self.embed_dim = 384
self.depth = 12
self.num_heads = 6
self.mlp_ratio = 4
self.qkv_bias = True
self.extract_layers = [3, 6, 9, 12]
self.input_channels = 3 # RGB
self.num_tissue_classes = num_tissue_classes
self.num_nuclei_classes = num_nuclei_classes
super().__init__(
num_nuclei_classes,
num_tissue_classes,
self.embed_dim,
self.input_channels,
self.depth,
self.num_heads,
self.extract_layers,
self.mlp_ratio,
self.qkv_bias,
drop_rate,
attn_drop_rate,
drop_path_rate,
regression_loss,
)
self.model256_path = model256_path
def load_pretrained_encoder(self, model256_path):
state_dict = torch.load(str(model256_path), map_location="cpu")["teacher"]
state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()}
state_dict = {k.replace("backbone.", ""): v for k, v in state_dict.items()}
msg = self.encoder.load_state_dict(state_dict, strict=False)
print(f"Loading checkpoint: {msg}")
class CellViTSAMShared(CellViTShared):
"""CellViT with SAM backbone settings
All heads are shared, just final layers are not shared
Args:
model_path (Union[Path, str]): Path to pretrained SAM model
num_nuclei_classes (int): Number of nuclei classes (including background)
num_tissue_classes (int): Number of tissue classes
vit_structure (Literal["SAM-B", "SAM-L", "SAM-H"]): SAM model type
drop_rate (float, optional): Dropout in MLP. Defaults to 0.
regression_loss (bool, optional): Use regressive loss for predicting vector components.
Adds two additional channels to the binary decoder, but returns it as own entry in dict. Defaults to False.
Raises:
NotImplementedError: Unknown SAM configuration
"""
def __init__(
self,
model_path: Union[Path, str],
num_nuclei_classes: int,
num_tissue_classes: int,
vit_structure: Literal["SAM-B", "SAM-L", "SAM-H"],
drop_rate: float = 0,
regression_loss: bool = False,
):
if vit_structure.upper() == "SAM-B":
self.init_vit_b()
elif vit_structure.upper() == "SAM-L":
self.init_vit_l()
elif vit_structure.upper() == "SAM-H":
self.init_vit_h()
else:
raise NotImplementedError("Unknown ViT-SAM backbone structure")
self.input_channels = 3 # RGB
self.mlp_ratio = 4
self.qkv_bias = True
self.model_path = model_path
super().__init__(
num_nuclei_classes=num_nuclei_classes,
num_tissue_classes=num_tissue_classes,
embed_dim=self.embed_dim,
input_channels=self.input_channels,
depth=self.depth,
num_heads=self.num_heads,
extract_layers=self.extract_layers,
mlp_ratio=self.mlp_ratio,
qkv_bias=self.qkv_bias,
drop_rate=drop_rate,
regression_loss=regression_loss,
)
self.prompt_embed_dim = 256
self.encoder = ViTCellViTDeit(
extract_layers=self.extract_layers,
depth=self.depth,
embed_dim=self.embed_dim,
mlp_ratio=4,
norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
num_heads=self.num_heads,
qkv_bias=True,
use_rel_pos=True,
global_attn_indexes=self.encoder_global_attn_indexes,
window_size=14,
out_chans=self.prompt_embed_dim,
)
self.classifier_head = (
nn.Linear(self.prompt_embed_dim, num_tissue_classes)
if num_tissue_classes > 0
else nn.Identity()
)
def load_pretrained_encoder(self, model_path):
"""Load pretrained SAM encoder from provided path
Args:
model_path (str): Path to SAM model
"""
state_dict = torch.load(str(model_path), map_location="cpu")
image_encoder = self.encoder
msg = image_encoder.load_state_dict(state_dict, strict=False)
print(f"Loading checkpoint: {msg}")
self.encoder = image_encoder
def forward(self, x: torch.Tensor, retrieve_tokens: bool = False):
"""Forward pass
Args:
x (torch.Tensor): Images in BCHW style
retrieve_tokens (bool, optional): If tokens of ViT should be returned as well. Defaults to False.
Returns:
dict: Output for all branches:
* tissue_types: Raw tissue type prediction. Shape: (B, num_tissue_classes)
* nuclei_binary_map: Raw binary cell segmentation predictions. Shape: (B, 2, H, W)
* hv_map: Binary HV Map predictions. Shape: (B, 2, H, W)
* nuclei_type_map: Raw binary nuclei type preditcions. Shape: (B, num_nuclei_classes, H, W)
* [Optional, if retrieve tokens]: tokens
* [Optional, if regression loss]:
* regression_map: Regression map for binary prediction. Shape: (B, 2, H, W)
"""
assert (
x.shape[-2] % self.patch_size == 0
), "Img must have a shape of that is divisble by patch_soze (token_size)"
assert (
x.shape[-1] % self.patch_size == 0
), "Img must have a shape of that is divisble by patch_soze (token_size)"
out_dict = {}
classifier_logits, _, z = self.encoder(x)
out_dict["tissue_types"] = self.classifier_head(classifier_logits)
z0, z1, z2, z3, z4 = x, *z
# performing reshape for the convolutional layers and upsampling (restore spatial dimension)
z4 = z4.permute(0, 3, 1, 2)
z3 = z3.permute(0, 3, 1, 2)
z2 = z2.permute(0, 3, 1, 2)
z1 = z1.permute(0, 3, 1, 2)
upsampled = self._forward_upsample(z0, z1, z2, z3, z4, self.decoder)
if self.regression_loss:
nb_map = self.nuclei_binary_map_decoder(upsampled)
out_dict["nuclei_binary_map"] = nb_map[:, :2, :, :]
out_dict["regression_map"] = nb_map[:, 2:, :, :]
else:
out_dict["nuclei_binary_map"] = self.nuclei_binary_map_decoder(upsampled)
out_dict["hv_map"] = self.hv_map_decoder(upsampled)
out_dict["nuclei_type_map"] = self.nuclei_type_maps_decoder(upsampled)
if retrieve_tokens:
out_dict["tokens"] = z4
return out_dict
def init_vit_b(self):
self.embed_dim = 768
self.depth = 12
self.num_heads = 12
self.encoder_global_attn_indexes = [2, 5, 8, 11]
self.extract_layers = [3, 6, 9, 12]
def init_vit_l(self):
self.embed_dim = 1024
self.depth = 24
self.num_heads = 16
self.encoder_global_attn_indexes = [5, 11, 17, 23]
self.extract_layers = [6, 12, 18, 24]
def init_vit_h(self):
self.embed_dim = 1280
self.depth = 32
self.num_heads = 16
self.encoder_global_attn_indexes = [7, 15, 23, 31]
self.extract_layers = [8, 16, 24, 32]