Image Segmentation
Transformers
Safetensors
cond_unet
ultrasound
medical-image-segmentation
attention-unet
custom-pipeline
custom_code
Instructions to use AImageLab-Zip/US_Cond-UNet with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AImageLab-Zip/US_Cond-UNet with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-segmentation", model="AImageLab-Zip/US_Cond-UNet", trust_remote_code=True)# Load model directly from transformers import AutoModelForImageSegmentation model = AutoModelForImageSegmentation.from_pretrained("AImageLab-Zip/US_Cond-UNet", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- README.md +75 -0
- config.json +50 -0
- configuration_cond_unet.py +49 -0
- image_processing_cond_unet.py +79 -0
- model.safetensors +3 -0
- modeling_cond_unet.py +74 -0
- pipeline.py +50 -0
- preprocessor_config.json +15 -0
- requirements.txt +8 -0
- segm_net.py +1051 -0
- unet_attn.py +971 -0
- unet_base.py +386 -0
README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
pipeline_tag: image-segmentation
|
| 3 |
+
library_name: transformers
|
| 4 |
+
tags:
|
| 5 |
+
- ultrasound
|
| 6 |
+
- medical-image-segmentation
|
| 7 |
+
- attention-unet
|
| 8 |
+
- custom-pipeline
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Cond-UNet Attention for Ultrasound Segmentation
|
| 12 |
+
|
| 13 |
+
Cond-UNet Attention is a binary ultrasound segmentation model based on an
|
| 14 |
+
attention-conditioned U-Net. It was trained to predict a foreground mask from
|
| 15 |
+
an RGB ultrasound image.
|
| 16 |
+
|
| 17 |
+
## Model Details
|
| 18 |
+
|
| 19 |
+
- Architecture: attention-conditioned U-Net
|
| 20 |
+
- U-Net depth: 5
|
| 21 |
+
- Base channels: 16
|
| 22 |
+
- Input resolution: 512 x 512
|
| 23 |
+
- Attention embedding dimension: 768
|
| 24 |
+
- Attention patch size: 8
|
| 25 |
+
- Output: one foreground logit per pixel
|
| 26 |
+
- Organ conditioning: optional; omitted IDs use the unknown-organ token (`-1`)
|
| 27 |
+
|
| 28 |
+
The model does not use DWT features or the optional shape-conditioning branch.
|
| 29 |
+
|
| 30 |
+
## Usage
|
| 31 |
+
|
| 32 |
+
This repository contains custom Transformers code. Pass `trust_remote_code=True`
|
| 33 |
+
when loading it.
|
| 34 |
+
|
| 35 |
+
```python
|
| 36 |
+
from transformers import pipeline
|
| 37 |
+
|
| 38 |
+
segmenter = pipeline(
|
| 39 |
+
"image-segmentation",
|
| 40 |
+
model="YOUR_ORG/YOUR_MODEL",
|
| 41 |
+
trust_remote_code=True,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
result = segmenter("ultrasound.png")
|
| 45 |
+
mask = result["mask"]
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
When organ metadata is known, pass its integer class ID:
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
result = segmenter("ultrasound.png", organ_id=3)
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
If `organ_id` is not provided, the model automatically uses `-1`, matching the
|
| 55 |
+
unknown-organ conditioning used in training.
|
| 56 |
+
|
| 57 |
+
## Output
|
| 58 |
+
|
| 59 |
+
The pipeline applies a sigmoid to the foreground logit and returns a binary
|
| 60 |
+
PIL mask thresholded at 0.5. Change the threshold if needed:
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
result = segmenter("ultrasound.png", threshold=0.4)
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Preprocessing
|
| 67 |
+
|
| 68 |
+
Images are converted to RGB, resized directly to 512 x 512, and normalized in
|
| 69 |
+
the 0-255 value range with mean `[123.675, 116.28, 103.53]` and standard
|
| 70 |
+
deviation `[58.395, 57.12, 57.375]`.
|
| 71 |
+
|
| 72 |
+
## Limitations
|
| 73 |
+
|
| 74 |
+
This model is intended for research use only. It is not a clinical diagnostic
|
| 75 |
+
device and must not be used as the sole basis for medical decisions.
|
config.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"CondUNetForSemanticSegmentation"
|
| 4 |
+
],
|
| 5 |
+
"attn_start": 0,
|
| 6 |
+
"auto_map": {
|
| 7 |
+
"AutoConfig": "configuration_cond_unet.CondUNetConfig",
|
| 8 |
+
"AutoImageProcessor": "image_processing_cond_unet.CondUNetImageProcessor",
|
| 9 |
+
"AutoModelForImageSegmentation": "modeling_cond_unet.CondUNetForSemanticSegmentation"
|
| 10 |
+
},
|
| 11 |
+
"custom_pipelines": {
|
| 12 |
+
"image-segmentation": {
|
| 13 |
+
"impl": "pipeline.CondUNetImageSegmentationPipeline",
|
| 14 |
+
"pt": [
|
| 15 |
+
"AutoModelForImageSegmentation"
|
| 16 |
+
],
|
| 17 |
+
"type": "image"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"depth": 5,
|
| 21 |
+
"dtype": "float32",
|
| 22 |
+
"dwt_bands": [
|
| 23 |
+
"LL",
|
| 24 |
+
"LH",
|
| 25 |
+
"HL",
|
| 26 |
+
"HH"
|
| 27 |
+
],
|
| 28 |
+
"emb_dim": 768,
|
| 29 |
+
"id2label": {
|
| 30 |
+
"0": "foreground"
|
| 31 |
+
},
|
| 32 |
+
"image_size": 512,
|
| 33 |
+
"in_channels": 3,
|
| 34 |
+
"keep_aspect_ratio": false,
|
| 35 |
+
"label2id": {
|
| 36 |
+
"foreground": 0
|
| 37 |
+
},
|
| 38 |
+
"model_type": "cond_unet",
|
| 39 |
+
"n_heads": 8,
|
| 40 |
+
"n_organs": 10,
|
| 41 |
+
"patch_size": 8,
|
| 42 |
+
"shape_res": 32,
|
| 43 |
+
"size": 16,
|
| 44 |
+
"transformers_version": "5.16.1",
|
| 45 |
+
"unknown_organ_id": -1,
|
| 46 |
+
"use_attn": true,
|
| 47 |
+
"use_dwt": false,
|
| 48 |
+
"use_shape": false,
|
| 49 |
+
"wavelet": "haar"
|
| 50 |
+
}
|
configuration_cond_unet.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import PretrainedConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class CondUNetConfig(PretrainedConfig):
|
| 5 |
+
model_type = "cond_unet"
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
in_channels=3,
|
| 10 |
+
num_labels=1,
|
| 11 |
+
n_organs=10,
|
| 12 |
+
size=32,
|
| 13 |
+
depth=5,
|
| 14 |
+
attn_start=0,
|
| 15 |
+
use_attn=True,
|
| 16 |
+
image_size=512,
|
| 17 |
+
patch_size=8,
|
| 18 |
+
emb_dim=768,
|
| 19 |
+
n_heads=8,
|
| 20 |
+
use_dwt=False,
|
| 21 |
+
wavelet="haar",
|
| 22 |
+
dwt_bands=None,
|
| 23 |
+
use_shape=False,
|
| 24 |
+
shape_res=32,
|
| 25 |
+
unknown_organ_id=-1,
|
| 26 |
+
keep_aspect_ratio=False,
|
| 27 |
+
**kwargs,
|
| 28 |
+
):
|
| 29 |
+
super().__init__(**kwargs)
|
| 30 |
+
self.in_channels = in_channels
|
| 31 |
+
self.num_labels = num_labels
|
| 32 |
+
self.n_organs = n_organs
|
| 33 |
+
self.size = size
|
| 34 |
+
self.depth = depth
|
| 35 |
+
self.attn_start = attn_start
|
| 36 |
+
self.use_attn = use_attn
|
| 37 |
+
self.image_size = image_size
|
| 38 |
+
self.patch_size = patch_size
|
| 39 |
+
self.emb_dim = emb_dim
|
| 40 |
+
self.n_heads = n_heads
|
| 41 |
+
self.use_dwt = use_dwt
|
| 42 |
+
self.wavelet = wavelet
|
| 43 |
+
self.dwt_bands = dwt_bands or ["LL", "LH", "HL", "HH"]
|
| 44 |
+
self.use_shape = use_shape
|
| 45 |
+
self.shape_res = shape_res
|
| 46 |
+
self.unknown_organ_id = unknown_organ_id
|
| 47 |
+
self.keep_aspect_ratio = keep_aspect_ratio
|
| 48 |
+
self.id2label = {0: "foreground"}
|
| 49 |
+
self.label2id = {"foreground": 0}
|
image_processing_cond_unet.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Union
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CondUNetImageProcessor(BaseImageProcessor):
|
| 11 |
+
model_input_names = ["pixel_values"]
|
| 12 |
+
|
| 13 |
+
def __init__(
|
| 14 |
+
self,
|
| 15 |
+
image_size=512,
|
| 16 |
+
keep_aspect_ratio=False,
|
| 17 |
+
mean=None,
|
| 18 |
+
std=None,
|
| 19 |
+
**kwargs,
|
| 20 |
+
):
|
| 21 |
+
super().__init__(**kwargs)
|
| 22 |
+
self.image_size = image_size
|
| 23 |
+
self.keep_aspect_ratio = keep_aspect_ratio
|
| 24 |
+
self.mean = mean or [123.675, 116.28, 103.53]
|
| 25 |
+
self.std = std or [58.395, 57.12, 57.375]
|
| 26 |
+
|
| 27 |
+
def preprocess(
|
| 28 |
+
self,
|
| 29 |
+
images: Union[Image.Image, np.ndarray, torch.Tensor, list],
|
| 30 |
+
return_tensors: Optional[Union[str, torch.Tensor]] = None,
|
| 31 |
+
**kwargs,
|
| 32 |
+
):
|
| 33 |
+
if not isinstance(images, (list, tuple)):
|
| 34 |
+
images = [images]
|
| 35 |
+
pixel_values = [self._preprocess_image(image) for image in images]
|
| 36 |
+
return BatchFeature(data={"pixel_values": pixel_values}, tensor_type=return_tensors)
|
| 37 |
+
|
| 38 |
+
def _preprocess_image(self, image):
|
| 39 |
+
if isinstance(image, Image.Image):
|
| 40 |
+
image = np.array(image.convert("RGB"), copy=True)
|
| 41 |
+
if isinstance(image, np.ndarray):
|
| 42 |
+
image = torch.from_numpy(image)
|
| 43 |
+
if image.ndim != 3:
|
| 44 |
+
raise ValueError("Expected an HWC or CHW RGB image.")
|
| 45 |
+
if image.shape[-1] in (1, 3):
|
| 46 |
+
image = image.permute(2, 0, 1)
|
| 47 |
+
if image.shape[0] == 1:
|
| 48 |
+
image = image.expand(3, -1, -1)
|
| 49 |
+
if image.shape[0] != 3:
|
| 50 |
+
raise ValueError("Cond-UNet requires one or three input channels.")
|
| 51 |
+
image = image.to(dtype=torch.float32)
|
| 52 |
+
if image.max() <= 1:
|
| 53 |
+
image = image * 255.0
|
| 54 |
+
height, width = image.shape[-2:]
|
| 55 |
+
if self.keep_aspect_ratio:
|
| 56 |
+
scale = self.image_size / max(height, width)
|
| 57 |
+
new_height = int(height * scale)
|
| 58 |
+
new_width = int(width * scale)
|
| 59 |
+
new_height += new_height % 2
|
| 60 |
+
new_width += new_width % 2
|
| 61 |
+
image = F.interpolate(
|
| 62 |
+
image.unsqueeze(0),
|
| 63 |
+
size=(new_height, new_width),
|
| 64 |
+
mode="bilinear",
|
| 65 |
+
align_corners=False,
|
| 66 |
+
).squeeze(0)
|
| 67 |
+
pad_left = (self.image_size - new_width) // 2
|
| 68 |
+
pad_top = (self.image_size - new_height) // 2
|
| 69 |
+
image = F.pad(image, (pad_left, self.image_size - new_width - pad_left, pad_top, self.image_size - new_height - pad_top))
|
| 70 |
+
else:
|
| 71 |
+
image = F.interpolate(
|
| 72 |
+
image.unsqueeze(0),
|
| 73 |
+
size=(self.image_size, self.image_size),
|
| 74 |
+
mode="bilinear",
|
| 75 |
+
align_corners=False,
|
| 76 |
+
).squeeze(0)
|
| 77 |
+
mean = torch.tensor(self.mean, dtype=image.dtype).view(-1, 1, 1)
|
| 78 |
+
std = torch.tensor(self.std, dtype=image.dtype).view(-1, 1, 1)
|
| 79 |
+
return (image - mean) / std
|
model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7dabb114d6c30c42b81eab8354cade8fd9f8335cc70dc05070b4b65e4ac7f747
|
| 3 |
+
size 191410692
|
modeling_cond_unet.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from transformers import PreTrainedModel
|
| 5 |
+
from transformers.modeling_outputs import SemanticSegmenterOutput
|
| 6 |
+
|
| 7 |
+
from .configuration_cond_unet import CondUNetConfig
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from .unet_attn import UNet2DAttn
|
| 11 |
+
except ModuleNotFoundError:
|
| 12 |
+
# The release exporter bundles these modules into the Hub repository root.
|
| 13 |
+
from nets.unet_attn import UNet2DAttn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class CondUNetForSemanticSegmentation(PreTrainedModel):
|
| 17 |
+
config_class = CondUNetConfig
|
| 18 |
+
main_input_name = "pixel_values"
|
| 19 |
+
|
| 20 |
+
def __init__(self, config: CondUNetConfig):
|
| 21 |
+
super().__init__(config)
|
| 22 |
+
self.unet = UNet2DAttn(
|
| 23 |
+
in_channels=config.in_channels,
|
| 24 |
+
num_classes=config.num_labels,
|
| 25 |
+
n_organs=config.n_organs,
|
| 26 |
+
size=config.size,
|
| 27 |
+
depth=config.depth,
|
| 28 |
+
attn_start=config.attn_start,
|
| 29 |
+
use_attn=config.use_attn,
|
| 30 |
+
img_size=config.image_size,
|
| 31 |
+
patch_size=config.patch_size,
|
| 32 |
+
emb_dim=config.emb_dim,
|
| 33 |
+
n_heads=config.n_heads,
|
| 34 |
+
distill=False,
|
| 35 |
+
distill_unet=False,
|
| 36 |
+
use_dwt=config.use_dwt,
|
| 37 |
+
wavelet=config.wavelet,
|
| 38 |
+
dwt_bands=config.dwt_bands,
|
| 39 |
+
use_shape=config.use_shape,
|
| 40 |
+
shape_res=config.shape_res,
|
| 41 |
+
)
|
| 42 |
+
self.post_init()
|
| 43 |
+
|
| 44 |
+
def forward(
|
| 45 |
+
self,
|
| 46 |
+
pixel_values: torch.FloatTensor,
|
| 47 |
+
organ_id: Optional[torch.LongTensor] = None,
|
| 48 |
+
labels: Optional[torch.FloatTensor] = None,
|
| 49 |
+
return_dict: Optional[bool] = None,
|
| 50 |
+
**kwargs,
|
| 51 |
+
):
|
| 52 |
+
if organ_id is None:
|
| 53 |
+
organ_id = torch.full(
|
| 54 |
+
(pixel_values.shape[0],),
|
| 55 |
+
self.config.unknown_organ_id,
|
| 56 |
+
device=pixel_values.device,
|
| 57 |
+
dtype=torch.long,
|
| 58 |
+
)
|
| 59 |
+
else:
|
| 60 |
+
organ_id = organ_id.to(device=pixel_values.device, dtype=torch.long)
|
| 61 |
+
|
| 62 |
+
outputs = self.unet(
|
| 63 |
+
pixel_values=pixel_values,
|
| 64 |
+
organ_id=organ_id,
|
| 65 |
+
masks=labels,
|
| 66 |
+
**kwargs,
|
| 67 |
+
)
|
| 68 |
+
logits = outputs["logits"]
|
| 69 |
+
if logits.ndim == 3:
|
| 70 |
+
logits = logits.unsqueeze(1)
|
| 71 |
+
loss = outputs["loss"] if labels is not None else None
|
| 72 |
+
if return_dict is False:
|
| 73 |
+
return (loss, logits) if loss is not None else (logits,)
|
| 74 |
+
return SemanticSegmenterOutput(loss=loss, logits=logits)
|
pipeline.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from transformers.pipelines.base import Pipeline
|
| 6 |
+
|
| 7 |
+
from .image_processing_cond_unet import CondUNetImageProcessor
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CondUNetImageSegmentationPipeline(Pipeline):
|
| 11 |
+
def __init__(self, *args, **kwargs):
|
| 12 |
+
super().__init__(*args, **kwargs)
|
| 13 |
+
if self.image_processor is None:
|
| 14 |
+
self.image_processor = CondUNetImageProcessor(
|
| 15 |
+
image_size=self.model.config.image_size,
|
| 16 |
+
keep_aspect_ratio=self.model.config.keep_aspect_ratio,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def _sanitize_parameters(self, organ_id=None, threshold=None, **kwargs):
|
| 20 |
+
preprocess_kwargs = {}
|
| 21 |
+
postprocess_kwargs = {}
|
| 22 |
+
if organ_id is not None:
|
| 23 |
+
preprocess_kwargs["organ_id"] = organ_id
|
| 24 |
+
if threshold is not None:
|
| 25 |
+
postprocess_kwargs["threshold"] = threshold
|
| 26 |
+
return preprocess_kwargs, {}, postprocess_kwargs
|
| 27 |
+
|
| 28 |
+
def preprocess(self, image, organ_id=None, **kwargs):
|
| 29 |
+
if not isinstance(image, Image.Image):
|
| 30 |
+
image = Image.open(image).convert("RGB")
|
| 31 |
+
else:
|
| 32 |
+
image = image.convert("RGB")
|
| 33 |
+
width, height = image.size
|
| 34 |
+
inputs = self.image_processor(images=image, return_tensors="pt")
|
| 35 |
+
inputs["original_size"] = (height, width)
|
| 36 |
+
if organ_id is not None:
|
| 37 |
+
inputs["organ_id"] = torch.tensor([organ_id], dtype=torch.long)
|
| 38 |
+
return inputs
|
| 39 |
+
|
| 40 |
+
def _forward(self, model_inputs, **kwargs):
|
| 41 |
+
original_size = model_inputs.pop("original_size")
|
| 42 |
+
outputs = self.model(**model_inputs)
|
| 43 |
+
return {"logits": outputs.logits, "original_size": original_size}
|
| 44 |
+
|
| 45 |
+
def postprocess(self, model_outputs, threshold=0.5, **kwargs):
|
| 46 |
+
logits = model_outputs["logits"]
|
| 47 |
+
height, width = model_outputs["original_size"]
|
| 48 |
+
probabilities = torch.sigmoid(F.interpolate(logits, size=(height, width), mode="bilinear", align_corners=False))[0, 0]
|
| 49 |
+
mask = (probabilities >= threshold).to(torch.uint8).cpu().numpy() * 255
|
| 50 |
+
return {"label": "foreground", "mask": Image.fromarray(mask), "score": float(probabilities.mean())}
|
preprocessor_config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"image_processor_type": "CondUNetImageProcessor",
|
| 3 |
+
"image_size": 512,
|
| 4 |
+
"keep_aspect_ratio": false,
|
| 5 |
+
"mean": [
|
| 6 |
+
123.675,
|
| 7 |
+
116.28,
|
| 8 |
+
103.53
|
| 9 |
+
],
|
| 10 |
+
"std": [
|
| 11 |
+
58.395,
|
| 12 |
+
57.12,
|
| 13 |
+
57.375
|
| 14 |
+
]
|
| 15 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=4.40
|
| 2 |
+
torch
|
| 3 |
+
torchvision
|
| 4 |
+
safetensors
|
| 5 |
+
einops
|
| 6 |
+
ptwt
|
| 7 |
+
wandb
|
| 8 |
+
peft
|
segm_net.py
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
import wandb
|
| 6 |
+
from peft import LoraConfig, get_peft_model
|
| 7 |
+
from torchvision.transforms import v2
|
| 8 |
+
from .unet_base import BaseUnet
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class SplitQKVLinear(nn.Module):
|
| 12 |
+
def __init__(self, base_layer: nn.Linear):
|
| 13 |
+
super().__init__()
|
| 14 |
+
if base_layer.out_features % 3 != 0:
|
| 15 |
+
raise ValueError("Expected fused qkv projection with out_features divisible by 3.")
|
| 16 |
+
|
| 17 |
+
self.proj_dim = base_layer.out_features // 3
|
| 18 |
+
self.q_proj = nn.Linear(base_layer.in_features, self.proj_dim, bias=base_layer.bias is not None)
|
| 19 |
+
self.k_proj = nn.Linear(base_layer.in_features, self.proj_dim, bias=base_layer.bias is not None)
|
| 20 |
+
self.v_proj = nn.Linear(base_layer.in_features, self.proj_dim, bias=base_layer.bias is not None)
|
| 21 |
+
|
| 22 |
+
with torch.no_grad():
|
| 23 |
+
self.q_proj.weight.copy_(base_layer.weight[: self.proj_dim])
|
| 24 |
+
self.k_proj.weight.copy_(base_layer.weight[self.proj_dim : 2 * self.proj_dim])
|
| 25 |
+
self.v_proj.weight.copy_(base_layer.weight[2 * self.proj_dim :])
|
| 26 |
+
|
| 27 |
+
if base_layer.bias is not None:
|
| 28 |
+
self.q_proj.bias.copy_(base_layer.bias[: self.proj_dim])
|
| 29 |
+
self.k_proj.bias.copy_(base_layer.bias[self.proj_dim : 2 * self.proj_dim])
|
| 30 |
+
self.v_proj.bias.copy_(base_layer.bias[2 * self.proj_dim :])
|
| 31 |
+
|
| 32 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 33 |
+
q = self.q_proj(x)
|
| 34 |
+
k = self.k_proj(x)
|
| 35 |
+
v = self.v_proj(x)
|
| 36 |
+
return torch.cat((q, k, v), dim=-1)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def replace_qkv_with_split_projections(module: nn.Module):
|
| 40 |
+
replaced_layers = 0
|
| 41 |
+
for child_name, child_module in list(module.named_children()):
|
| 42 |
+
if (
|
| 43 |
+
child_name == "qkv"
|
| 44 |
+
and isinstance(child_module, nn.Linear)
|
| 45 |
+
and child_module.out_features == child_module.in_features * 3
|
| 46 |
+
):
|
| 47 |
+
setattr(module, child_name, SplitQKVLinear(child_module))
|
| 48 |
+
replaced_layers += 1
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
replaced_layers += replace_qkv_with_split_projections(child_module)
|
| 52 |
+
|
| 53 |
+
return replaced_layers
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def apply_lora_to_qv_projections(module: nn.Module, rank: int, alpha: float | None = None):
|
| 57 |
+
replaced_layers = replace_qkv_with_split_projections(module)
|
| 58 |
+
if replaced_layers == 0:
|
| 59 |
+
raise ValueError("No qkv projections found in MedSAM image encoder for LoRA injection.")
|
| 60 |
+
|
| 61 |
+
lora_config = LoraConfig(
|
| 62 |
+
r=rank,
|
| 63 |
+
lora_alpha=(alpha if alpha is not None else rank),
|
| 64 |
+
target_modules=["q_proj", "v_proj"],
|
| 65 |
+
bias="none",
|
| 66 |
+
)
|
| 67 |
+
peft_module = get_peft_model(module, lora_config)
|
| 68 |
+
return peft_module, replaced_layers
|
| 69 |
+
|
| 70 |
+
def pad_to_2d(x: torch.Tensor, stride: int):
|
| 71 |
+
h, w = x.shape[-2:]
|
| 72 |
+
|
| 73 |
+
new_h = h if h % stride == 0 else h + stride - (h % stride)
|
| 74 |
+
new_w = w if w % stride == 0 else w + stride - (w % stride)
|
| 75 |
+
|
| 76 |
+
top = (new_h - h) // 2
|
| 77 |
+
bottom = (new_h - h) - top
|
| 78 |
+
left = (new_w - w) // 2
|
| 79 |
+
right = (new_w - w) - left
|
| 80 |
+
|
| 81 |
+
pads = (left, right, top, bottom)
|
| 82 |
+
x_pad = F.pad(x, pads, mode="constant", value=0)
|
| 83 |
+
return x_pad, pads
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def unpad_2d(x: torch.Tensor, pads):
|
| 87 |
+
left, right, top, bottom = pads
|
| 88 |
+
|
| 89 |
+
if top or bottom:
|
| 90 |
+
end_h = -bottom if bottom > 0 else None
|
| 91 |
+
x = x[:, :, top:end_h, :]
|
| 92 |
+
|
| 93 |
+
if left or right:
|
| 94 |
+
end_w = -right if right > 0 else None
|
| 95 |
+
x = x[:, :, :, left:end_w]
|
| 96 |
+
|
| 97 |
+
return x
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class ConvBlock(nn.Module):
|
| 101 |
+
def __init__(
|
| 102 |
+
self,
|
| 103 |
+
in_channels,
|
| 104 |
+
out_channels,
|
| 105 |
+
conv_kwargs={"kernel_size": 3, "stride": 1, "padding": 1},
|
| 106 |
+
):
|
| 107 |
+
super(ConvBlock, self).__init__()
|
| 108 |
+
self.block = nn.Sequential(
|
| 109 |
+
nn.Conv2d(in_channels, out_channels, **conv_kwargs),
|
| 110 |
+
nn.InstanceNorm2d(out_channels),
|
| 111 |
+
nn.LeakyReLU(),
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
def forward(self, x):
|
| 115 |
+
return self.block(x)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# Single encoder block
|
| 119 |
+
class DownConvBlock(nn.Module):
|
| 120 |
+
def __init__(
|
| 121 |
+
self,
|
| 122 |
+
in_channels: list,
|
| 123 |
+
out_channels: list,
|
| 124 |
+
conv_kwargs={"kernel_size": 3, "stride": 1, "padding": 1},
|
| 125 |
+
):
|
| 126 |
+
super(DownConvBlock, self).__init__()
|
| 127 |
+
|
| 128 |
+
assert len(in_channels) == len(
|
| 129 |
+
out_channels
|
| 130 |
+
), f"in_channels length is {len(in_channels)} while out_channels is {len(out_channels)}"
|
| 131 |
+
|
| 132 |
+
# Variable number of convolutional block in each layer, based on the in_channels and out_channels length
|
| 133 |
+
self.conv_blocks = nn.ModuleList(
|
| 134 |
+
[
|
| 135 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 136 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 137 |
+
]
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
self.pool = nn.MaxPool2d(2, 2)
|
| 141 |
+
|
| 142 |
+
def forward(self, x):
|
| 143 |
+
for block in self.conv_blocks:
|
| 144 |
+
x = block(x)
|
| 145 |
+
return self.pool(x), x
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# Single decoder block
|
| 149 |
+
class UpConvBlock(nn.Module):
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
in_channels: list,
|
| 153 |
+
out_channels: list,
|
| 154 |
+
up_conv=True,
|
| 155 |
+
conv_kwargs={"kernel_size": 3, "stride": 1, "padding": 1},
|
| 156 |
+
upconv_kwargs={"kernel_size": 2, "stride": 2},
|
| 157 |
+
):
|
| 158 |
+
super(UpConvBlock, self).__init__()
|
| 159 |
+
|
| 160 |
+
assert len(in_channels) == len(
|
| 161 |
+
out_channels
|
| 162 |
+
), f"in_channels length is {len(in_channels)} while out_channels is {len(out_channels)}"
|
| 163 |
+
|
| 164 |
+
# Variable number of convolutional block in each layer, based on the in_channels and out_channels length
|
| 165 |
+
self.conv_blocks = nn.ModuleList(
|
| 166 |
+
[
|
| 167 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 168 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 169 |
+
]
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
self.up_conv = up_conv
|
| 173 |
+
if self.up_conv:
|
| 174 |
+
self.up_conv_op = nn.ConvTranspose2d(
|
| 175 |
+
out_channels[-1], out_channels[-1], **upconv_kwargs
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
def forward(self, x):
|
| 179 |
+
for block in self.conv_blocks:
|
| 180 |
+
x = block(x)
|
| 181 |
+
|
| 182 |
+
if self.up_conv:
|
| 183 |
+
return self.up_conv_op(x)
|
| 184 |
+
else:
|
| 185 |
+
return x
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class FiLM2d(nn.Module):
|
| 189 |
+
"""
|
| 190 |
+
Feature-wise Linear Modulation for a 2-D feature map.
|
| 191 |
+
(γ, β) are generated from a learned embedding of organ_id.
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
def __init__(
|
| 195 |
+
self,
|
| 196 |
+
n_organs: int,
|
| 197 |
+
in_channels: int,
|
| 198 |
+
emb_dim: int | None = None,
|
| 199 |
+
hidden: int | None = None,
|
| 200 |
+
):
|
| 201 |
+
super().__init__()
|
| 202 |
+
hidden = hidden or 2 * in_channels
|
| 203 |
+
self.embed = nn.Embedding(n_organs + 1, emb_dim)
|
| 204 |
+
|
| 205 |
+
self.mlp = nn.Sequential(
|
| 206 |
+
nn.Linear(emb_dim, hidden),
|
| 207 |
+
nn.ReLU(inplace=True),
|
| 208 |
+
nn.Linear(hidden, 2 * in_channels), # → [β‖γ]
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
# initialise so that FiLM starts as identity: γ≈1, β≈0
|
| 212 |
+
nn.init.zeros_(self.mlp[-1].weight)
|
| 213 |
+
nn.init.constant_(self.mlp[-1].bias[:in_channels], 0) # β
|
| 214 |
+
nn.init.constant_(self.mlp[-1].bias[in_channels:], 1) # γ
|
| 215 |
+
|
| 216 |
+
def compute_modulation(self, organ_id: torch.Tensor):
|
| 217 |
+
"""Return FiLM coefficients and conditioning embedding for a batch."""
|
| 218 |
+
B = organ_id.shape[0]
|
| 219 |
+
device = organ_id.device
|
| 220 |
+
mask = organ_id >= 0 # [B]
|
| 221 |
+
q_org = self.embed(organ_id.clamp(min=0)) # [B, D] (dummy for unknown)
|
| 222 |
+
q_img = self.embed(
|
| 223 |
+
torch.tensor([self.embed.weight.shape[0] - 1], device=device).expand(B)
|
| 224 |
+
)
|
| 225 |
+
q = torch.where(mask[:, None], q_org, q_img)
|
| 226 |
+
beta_gamma = self.mlp(q) # (B, 2C)
|
| 227 |
+
beta, gamma = beta_gamma.chunk(2, dim=-1) # each (B, C)
|
| 228 |
+
beta = beta.unsqueeze(-1).unsqueeze(-1)
|
| 229 |
+
gamma = gamma.unsqueeze(-1).unsqueeze(-1)
|
| 230 |
+
return gamma, beta, q
|
| 231 |
+
|
| 232 |
+
def forward(self, x: torch.Tensor, organ_id: torch.Tensor):
|
| 233 |
+
"""
|
| 234 |
+
x : (B, C, H, W)
|
| 235 |
+
organ_id : (B,) integer 0…n_organs-1
|
| 236 |
+
"""
|
| 237 |
+
gamma, beta, _ = self.compute_modulation(organ_id)
|
| 238 |
+
return gamma * x + beta
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class SharedFiLMModulator:
|
| 242 |
+
"""
|
| 243 |
+
Compatibility helper that exposes the same collection interface used by
|
| 244 |
+
UNet2DAttn notebooks: compute all per-layer FiLM coefficients in one call.
|
| 245 |
+
"""
|
| 246 |
+
|
| 247 |
+
def __init__(self, model: "UNet2DFiLM"):
|
| 248 |
+
self.model = model
|
| 249 |
+
|
| 250 |
+
def compute_all_modulations(
|
| 251 |
+
self,
|
| 252 |
+
original_img: torch.Tensor,
|
| 253 |
+
organ_id: torch.Tensor,
|
| 254 |
+
layer_configs: list[tuple[int, int]],
|
| 255 |
+
return_compact: bool = False,
|
| 256 |
+
):
|
| 257 |
+
del original_img # FiLM modulation depends only on organ_id in this model.
|
| 258 |
+
|
| 259 |
+
modulations = {}
|
| 260 |
+
compact_modulations = {} if return_compact else None
|
| 261 |
+
film_layers = self.model._get_film_layers_in_order()
|
| 262 |
+
|
| 263 |
+
if len(film_layers) != len(layer_configs):
|
| 264 |
+
raise RuntimeError(
|
| 265 |
+
f"FiLM layer/config mismatch: {len(film_layers)} layers vs "
|
| 266 |
+
f"{len(layer_configs)} configs."
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
for (layer_id, n_channels), film_layer in zip(layer_configs, film_layers):
|
| 270 |
+
gamma, beta, film_context = film_layer.compute_modulation(organ_id)
|
| 271 |
+
modulations[layer_id] = (gamma, beta)
|
| 272 |
+
|
| 273 |
+
if not return_compact:
|
| 274 |
+
continue
|
| 275 |
+
|
| 276 |
+
gamma_flat = gamma.squeeze(-1).squeeze(-1)
|
| 277 |
+
beta_flat = beta.squeeze(-1).squeeze(-1)
|
| 278 |
+
|
| 279 |
+
if n_channels != self.model.max_channels:
|
| 280 |
+
gamma_compact = F.interpolate(
|
| 281 |
+
gamma_flat.unsqueeze(1),
|
| 282 |
+
size=self.model.max_channels,
|
| 283 |
+
mode="linear",
|
| 284 |
+
align_corners=False,
|
| 285 |
+
).squeeze(1)
|
| 286 |
+
beta_compact = F.interpolate(
|
| 287 |
+
beta_flat.unsqueeze(1),
|
| 288 |
+
size=self.model.max_channels,
|
| 289 |
+
mode="linear",
|
| 290 |
+
align_corners=False,
|
| 291 |
+
).squeeze(1)
|
| 292 |
+
else:
|
| 293 |
+
gamma_compact = gamma_flat
|
| 294 |
+
beta_compact = beta_flat
|
| 295 |
+
|
| 296 |
+
compact_modulations[layer_id] = (
|
| 297 |
+
gamma_compact,
|
| 298 |
+
beta_compact,
|
| 299 |
+
film_context,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
projected_shapes = torch.empty(0, device=organ_id.device)
|
| 303 |
+
if return_compact:
|
| 304 |
+
return modulations, projected_shapes, compact_modulations
|
| 305 |
+
return modulations, projected_shapes
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class DownConvBlockFiLM(nn.Module):
|
| 309 |
+
"""
|
| 310 |
+
Conv → FiLM → Conv → FiLM → Pool.
|
| 311 |
+
Except for FiLM, the API and behaviour remain the same.
|
| 312 |
+
"""
|
| 313 |
+
|
| 314 |
+
def __init__(
|
| 315 |
+
self,
|
| 316 |
+
in_channels: list[int],
|
| 317 |
+
out_channels: list[int],
|
| 318 |
+
n_organs: int,
|
| 319 |
+
conv_kwargs={"kernel_size": 3, "stride": 1, "padding": 1},
|
| 320 |
+
emb_dim: int = 64,
|
| 321 |
+
):
|
| 322 |
+
super().__init__()
|
| 323 |
+
assert len(in_channels) == len(
|
| 324 |
+
out_channels
|
| 325 |
+
), f"in_channels length is {len(in_channels)} while out_channels is {len(out_channels)}"
|
| 326 |
+
|
| 327 |
+
self.conv_blocks = nn.ModuleList(
|
| 328 |
+
[
|
| 329 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 330 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 331 |
+
]
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
self.film_blocks = nn.ModuleList(
|
| 335 |
+
[
|
| 336 |
+
FiLM2d(
|
| 337 |
+
n_organs=n_organs,
|
| 338 |
+
in_channels=out_ch,
|
| 339 |
+
emb_dim=emb_dim,
|
| 340 |
+
)
|
| 341 |
+
for out_ch in out_channels
|
| 342 |
+
]
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
self.pool = nn.MaxPool2d(2, 2)
|
| 346 |
+
|
| 347 |
+
def forward(
|
| 348 |
+
self,
|
| 349 |
+
x: torch.Tensor,
|
| 350 |
+
organ_id: torch.Tensor | None = None,
|
| 351 |
+
gammas: list[torch.Tensor] | None = None,
|
| 352 |
+
betas: list[torch.Tensor] | None = None,
|
| 353 |
+
):
|
| 354 |
+
"""
|
| 355 |
+
organ_id : (B,) torch.long (0 = breast, 1 = thyroid, …)
|
| 356 |
+
"""
|
| 357 |
+
if (gammas is None) != (betas is None):
|
| 358 |
+
raise ValueError("gammas and betas must be provided together.")
|
| 359 |
+
|
| 360 |
+
for idx, (conv, film) in enumerate(zip(self.conv_blocks, self.film_blocks)):
|
| 361 |
+
x = conv(x) # usual conv-norm-ReLU
|
| 362 |
+
if gammas is None:
|
| 363 |
+
if organ_id is None:
|
| 364 |
+
raise ValueError("organ_id is required when FiLM modulations are not precomputed.")
|
| 365 |
+
x = film(x, organ_id) # FiLM modulation
|
| 366 |
+
else:
|
| 367 |
+
x = gammas[idx] * x + betas[idx]
|
| 368 |
+
return self.pool(x), x # (downsampled, skip-connection)
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
class UpConvBlockFiLM(nn.Module):
|
| 372 |
+
"""
|
| 373 |
+
Up-sampling block with FiLM conditioning.
|
| 374 |
+
|
| 375 |
+
Conv → FiLM → Conv → FiLM → (optional) ConvTranspose2d
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
def __init__(
|
| 379 |
+
self,
|
| 380 |
+
in_channels: list[int],
|
| 381 |
+
out_channels: list[int],
|
| 382 |
+
n_organs: int,
|
| 383 |
+
up_conv: bool = True,
|
| 384 |
+
conv_kwargs: dict = {"kernel_size": 3, "stride": 1, "padding": 1},
|
| 385 |
+
upconv_kwargs: dict = {"kernel_size": 2, "stride": 2},
|
| 386 |
+
emb_dim: int = 64,
|
| 387 |
+
):
|
| 388 |
+
super().__init__()
|
| 389 |
+
assert len(in_channels) == len(
|
| 390 |
+
out_channels
|
| 391 |
+
), f"in_channels length is {len(in_channels)} while out_channels is {len(out_channels)}"
|
| 392 |
+
|
| 393 |
+
self.conv_blocks = nn.ModuleList(
|
| 394 |
+
[
|
| 395 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 396 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 397 |
+
]
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
self.film_blocks = nn.ModuleList(
|
| 401 |
+
[
|
| 402 |
+
FiLM2d(
|
| 403 |
+
n_organs=n_organs,
|
| 404 |
+
in_channels=out_ch,
|
| 405 |
+
emb_dim=emb_dim,
|
| 406 |
+
)
|
| 407 |
+
for out_ch in out_channels
|
| 408 |
+
]
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
self.up_conv = up_conv
|
| 412 |
+
if self.up_conv:
|
| 413 |
+
self.up_conv_op = nn.ConvTranspose2d(
|
| 414 |
+
out_channels[-1], out_channels[-1], **upconv_kwargs
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
def forward(
|
| 418 |
+
self,
|
| 419 |
+
x: torch.Tensor,
|
| 420 |
+
organ_id: torch.Tensor | None = None,
|
| 421 |
+
gammas: list[torch.Tensor] | None = None,
|
| 422 |
+
betas: list[torch.Tensor] | None = None,
|
| 423 |
+
):
|
| 424 |
+
"""
|
| 425 |
+
x : (B, C, H, W)
|
| 426 |
+
organ_id : (B,) long tensor - 0=breast, 1=thyroid, …
|
| 427 |
+
"""
|
| 428 |
+
if (gammas is None) != (betas is None):
|
| 429 |
+
raise ValueError("gammas and betas must be provided together.")
|
| 430 |
+
|
| 431 |
+
for idx, (conv, film) in enumerate(zip(self.conv_blocks, self.film_blocks)):
|
| 432 |
+
x = conv(x)
|
| 433 |
+
if gammas is None:
|
| 434 |
+
if organ_id is None:
|
| 435 |
+
raise ValueError("organ_id is required when FiLM modulations are not precomputed.")
|
| 436 |
+
x = film(x, organ_id)
|
| 437 |
+
else:
|
| 438 |
+
x = gammas[idx] * x + betas[idx]
|
| 439 |
+
|
| 440 |
+
if self.up_conv:
|
| 441 |
+
x = self.up_conv_op(x)
|
| 442 |
+
|
| 443 |
+
return x
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
class UNet2DFiLM(BaseUnet):
|
| 447 |
+
def __init__(
|
| 448 |
+
self,
|
| 449 |
+
in_channels: int,
|
| 450 |
+
num_classes: int,
|
| 451 |
+
n_organs: int,
|
| 452 |
+
size: int = 32,
|
| 453 |
+
depth: int = 3,
|
| 454 |
+
*,
|
| 455 |
+
film_start: int = 0,
|
| 456 |
+
use_film: bool = True,
|
| 457 |
+
film_embed: int = 64,
|
| 458 |
+
distill: bool = False,
|
| 459 |
+
distill_unet: bool = False,
|
| 460 |
+
medsam_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/medsam_unfreezed/model.safetensors",
|
| 461 |
+
unet_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/unet5_attn/model.safetensors",
|
| 462 |
+
unet_teacher_kwargs: dict | None = None,
|
| 463 |
+
):
|
| 464 |
+
"""
|
| 465 |
+
UNet with symmetric FiLM conditioning in encoder and decoder.
|
| 466 |
+
"""
|
| 467 |
+
super().__init__(
|
| 468 |
+
in_channels=in_channels,
|
| 469 |
+
num_classes=num_classes,
|
| 470 |
+
n_organs=n_organs,
|
| 471 |
+
size=size,
|
| 472 |
+
depth=depth,
|
| 473 |
+
film_start=film_start,
|
| 474 |
+
use_film=use_film,
|
| 475 |
+
film_embed=film_embed,
|
| 476 |
+
distill=distill,
|
| 477 |
+
distill_unet=distill_unet,
|
| 478 |
+
medsam_teacher_ckpt=medsam_teacher_ckpt,
|
| 479 |
+
unet_teacher_ckpt=unet_teacher_ckpt,
|
| 480 |
+
unet_teacher_kwargs=unet_teacher_kwargs,
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
def _build_model(
|
| 484 |
+
self,
|
| 485 |
+
*,
|
| 486 |
+
film_start: int = 0,
|
| 487 |
+
use_film: bool = True,
|
| 488 |
+
film_embed: int = 64,
|
| 489 |
+
distill: bool = False,
|
| 490 |
+
distill_unet: bool = False,
|
| 491 |
+
medsam_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/medsam_unfreezed/model.safetensors",
|
| 492 |
+
unet_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/unet5_attn/model.safetensors",
|
| 493 |
+
unet_teacher_kwargs: dict | None = None,
|
| 494 |
+
**kwargs,
|
| 495 |
+
):
|
| 496 |
+
if kwargs:
|
| 497 |
+
unknown = ", ".join(sorted(kwargs.keys()))
|
| 498 |
+
raise TypeError(f"Unexpected UNet2DFiLM kwargs: {unknown}")
|
| 499 |
+
|
| 500 |
+
self.film_start = max(0, int(film_start))
|
| 501 |
+
self.use_film = bool(use_film)
|
| 502 |
+
self.use_attn = self.use_film
|
| 503 |
+
self.attn_start = self.film_start
|
| 504 |
+
self.film_embed = int(film_embed)
|
| 505 |
+
self.max_channels = self.size * (2 ** (self.depth + 1))
|
| 506 |
+
self.criterion = DiceBCELoss()
|
| 507 |
+
self.shared_attn = SharedFiLMModulator(self) if self.use_film else None
|
| 508 |
+
|
| 509 |
+
# ---------------- Encoder ----------------
|
| 510 |
+
self.encoder = nn.ModuleDict()
|
| 511 |
+
|
| 512 |
+
if self.use_film and 0 >= self.film_start:
|
| 513 |
+
self.encoder["0"] = DownConvBlockFiLM(
|
| 514 |
+
[self.in_channels, self.size],
|
| 515 |
+
[self.size, self.size * 2],
|
| 516 |
+
n_organs=self.n_organs,
|
| 517 |
+
emb_dim=self.film_embed,
|
| 518 |
+
)
|
| 519 |
+
else:
|
| 520 |
+
self.encoder["0"] = DownConvBlock(
|
| 521 |
+
[self.in_channels, self.size], [self.size, self.size * 2]
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
for i in range(1, self.depth):
|
| 525 |
+
in_ch = [self.size * (2**i), self.size * (2**i)]
|
| 526 |
+
out_ch = [self.size * (2**i), self.size * (2 ** (i + 1))]
|
| 527 |
+
key = str(i)
|
| 528 |
+
|
| 529 |
+
if self.use_film and i >= self.film_start:
|
| 530 |
+
self.encoder[key] = DownConvBlockFiLM(
|
| 531 |
+
in_ch,
|
| 532 |
+
out_ch,
|
| 533 |
+
n_organs=self.n_organs,
|
| 534 |
+
emb_dim=self.film_embed,
|
| 535 |
+
)
|
| 536 |
+
else:
|
| 537 |
+
self.encoder[key] = DownConvBlock(in_ch, out_ch)
|
| 538 |
+
|
| 539 |
+
# ---------------- Bottleneck ----------------
|
| 540 |
+
if self.use_film:
|
| 541 |
+
self.bottleneck = UpConvBlockFiLM(
|
| 542 |
+
[self.size * (2**self.depth), self.size * (2**self.depth)],
|
| 543 |
+
[self.size * (2**self.depth), self.size * (2 ** (self.depth + 1))],
|
| 544 |
+
n_organs=self.n_organs,
|
| 545 |
+
emb_dim=self.film_embed,
|
| 546 |
+
)
|
| 547 |
+
else:
|
| 548 |
+
self.bottleneck = UpConvBlock(
|
| 549 |
+
[self.size * (2**self.depth), self.size * (2**self.depth)],
|
| 550 |
+
[self.size * (2**self.depth), self.size * (2 ** (self.depth + 1))],
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
# ---------------- Decoder ----------------
|
| 554 |
+
self.decoder = nn.ModuleDict()
|
| 555 |
+
|
| 556 |
+
for i in range(self.depth, 1, -1):
|
| 557 |
+
use_film_at_level = self.use_film and (i - 1) >= self.film_start
|
| 558 |
+
|
| 559 |
+
if use_film_at_level:
|
| 560 |
+
self.decoder[str(i - 1)] = UpConvBlockFiLM(
|
| 561 |
+
[
|
| 562 |
+
self.size * (2 ** (i + 1)) + self.size * (2**i),
|
| 563 |
+
self.size * (2**i),
|
| 564 |
+
],
|
| 565 |
+
[self.size * (2**i), self.size * (2**i)],
|
| 566 |
+
n_organs=self.n_organs,
|
| 567 |
+
emb_dim=self.film_embed,
|
| 568 |
+
)
|
| 569 |
+
else:
|
| 570 |
+
self.decoder[str(i - 1)] = UpConvBlock(
|
| 571 |
+
[
|
| 572 |
+
self.size * (2 ** (i + 1)) + self.size * (2**i),
|
| 573 |
+
self.size * (2**i),
|
| 574 |
+
],
|
| 575 |
+
[self.size * (2**i), self.size * (2**i)],
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
if self.use_film and 0 >= self.film_start:
|
| 579 |
+
self.decoder["0"] = UpConvBlockFiLM(
|
| 580 |
+
[self.size * 4 + self.size * 2, self.size * 2],
|
| 581 |
+
[self.size * 2, self.size * 2],
|
| 582 |
+
n_organs=self.n_organs,
|
| 583 |
+
up_conv=False,
|
| 584 |
+
emb_dim=self.film_embed,
|
| 585 |
+
)
|
| 586 |
+
else:
|
| 587 |
+
self.decoder["0"] = UpConvBlock(
|
| 588 |
+
[self.size * 4 + self.size * 2, self.size * 2],
|
| 589 |
+
[self.size * 2, self.size * 2],
|
| 590 |
+
up_conv=False,
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
self.out_layer = ConvBlock(
|
| 594 |
+
self.size * 2,
|
| 595 |
+
self.out_channels,
|
| 596 |
+
conv_kwargs={"kernel_size": 1, "stride": 1, "padding": 0},
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
self._init_distillation(
|
| 600 |
+
distill=distill,
|
| 601 |
+
distill_unet=distill_unet,
|
| 602 |
+
medsam_teacher_ckpt=medsam_teacher_ckpt,
|
| 603 |
+
unet_teacher_ckpt=unet_teacher_ckpt,
|
| 604 |
+
unet_teacher_kwargs=unet_teacher_kwargs,
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
def _build_layer_configs(self):
|
| 608 |
+
"""Match UNet2DAttn layer ordering for notebook-side FiLM collection."""
|
| 609 |
+
configs = []
|
| 610 |
+
layer_id = 0
|
| 611 |
+
|
| 612 |
+
for i in range(self.depth):
|
| 613 |
+
if self.use_film and i >= self.film_start:
|
| 614 |
+
configs.append((layer_id, self.size * (2**i)))
|
| 615 |
+
layer_id += 1
|
| 616 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 617 |
+
layer_id += 1
|
| 618 |
+
|
| 619 |
+
if self.use_film:
|
| 620 |
+
configs.append((layer_id, self.size * (2**self.depth)))
|
| 621 |
+
layer_id += 1
|
| 622 |
+
configs.append((layer_id, self.size * (2 ** (self.depth + 1))))
|
| 623 |
+
layer_id += 1
|
| 624 |
+
|
| 625 |
+
for i in range(self.depth - 1, -1, -1):
|
| 626 |
+
if self.use_film and i >= self.film_start:
|
| 627 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 628 |
+
layer_id += 1
|
| 629 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 630 |
+
layer_id += 1
|
| 631 |
+
|
| 632 |
+
return configs
|
| 633 |
+
|
| 634 |
+
def _get_film_layers_in_order(self):
|
| 635 |
+
"""Return FiLM modules in the same order as `_build_layer_configs`."""
|
| 636 |
+
film_layers = []
|
| 637 |
+
|
| 638 |
+
for i in range(self.depth):
|
| 639 |
+
layer = self.encoder[str(i)]
|
| 640 |
+
if isinstance(layer, DownConvBlockFiLM):
|
| 641 |
+
film_layers.extend(layer.film_blocks)
|
| 642 |
+
|
| 643 |
+
if isinstance(self.bottleneck, UpConvBlockFiLM):
|
| 644 |
+
film_layers.extend(self.bottleneck.film_blocks)
|
| 645 |
+
|
| 646 |
+
for i in range(self.depth - 1, -1, -1):
|
| 647 |
+
layer = self.decoder[str(i)]
|
| 648 |
+
if isinstance(layer, UpConvBlockFiLM):
|
| 649 |
+
film_layers.extend(layer.film_blocks)
|
| 650 |
+
|
| 651 |
+
return film_layers
|
| 652 |
+
|
| 653 |
+
def _prepare_forward(
|
| 654 |
+
self,
|
| 655 |
+
*,
|
| 656 |
+
pixel_values: torch.Tensor,
|
| 657 |
+
organ_id: torch.Tensor | None = None,
|
| 658 |
+
**kwargs,
|
| 659 |
+
) -> dict:
|
| 660 |
+
forward_ctx = {
|
| 661 |
+
"mod_list": None,
|
| 662 |
+
"mod_idx": 0,
|
| 663 |
+
}
|
| 664 |
+
if self.use_film:
|
| 665 |
+
if organ_id is None:
|
| 666 |
+
raise ValueError("organ_id is required when FiLM is enabled.")
|
| 667 |
+
layer_configs = self._build_layer_configs()
|
| 668 |
+
film_layers = self._get_film_layers_in_order()
|
| 669 |
+
if len(film_layers) != len(layer_configs):
|
| 670 |
+
raise RuntimeError(
|
| 671 |
+
f"FiLM layer/config mismatch: {len(film_layers)} layers vs "
|
| 672 |
+
f"{len(layer_configs)} configs."
|
| 673 |
+
)
|
| 674 |
+
modulations = {
|
| 675 |
+
layer_id: film_layer.compute_modulation(organ_id)[:2]
|
| 676 |
+
for (layer_id, _), film_layer in zip(layer_configs, film_layers)
|
| 677 |
+
}
|
| 678 |
+
forward_ctx["mod_list"] = [modulations[i] for i in range(len(layer_configs))]
|
| 679 |
+
return forward_ctx
|
| 680 |
+
|
| 681 |
+
def _next_modulation(self, forward_ctx: dict):
|
| 682 |
+
mod_idx = forward_ctx["mod_idx"]
|
| 683 |
+
mod_list = forward_ctx["mod_list"]
|
| 684 |
+
gammas = [mod_list[mod_idx][0], mod_list[mod_idx + 1][0]]
|
| 685 |
+
betas = [mod_list[mod_idx][1], mod_list[mod_idx + 1][1]]
|
| 686 |
+
forward_ctx["mod_idx"] = mod_idx + 2
|
| 687 |
+
return gammas, betas
|
| 688 |
+
|
| 689 |
+
def _encode(
|
| 690 |
+
self,
|
| 691 |
+
layer,
|
| 692 |
+
x,
|
| 693 |
+
organ_id=None,
|
| 694 |
+
forward_ctx=None,
|
| 695 |
+
):
|
| 696 |
+
if isinstance(layer, DownConvBlockFiLM):
|
| 697 |
+
if forward_ctx is not None and forward_ctx.get("mod_list") is not None:
|
| 698 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 699 |
+
return layer(x, gammas=gammas, betas=betas)
|
| 700 |
+
return layer(x, organ_id=organ_id)
|
| 701 |
+
else:
|
| 702 |
+
return layer(x)
|
| 703 |
+
|
| 704 |
+
def _decode(
|
| 705 |
+
self,
|
| 706 |
+
layer,
|
| 707 |
+
x,
|
| 708 |
+
organ_id=None,
|
| 709 |
+
forward_ctx=None,
|
| 710 |
+
):
|
| 711 |
+
if isinstance(layer, UpConvBlockFiLM):
|
| 712 |
+
if forward_ctx is not None and forward_ctx.get("mod_list") is not None:
|
| 713 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 714 |
+
return layer(x, gammas=gammas, betas=betas)
|
| 715 |
+
return layer(x, organ_id=organ_id)
|
| 716 |
+
else:
|
| 717 |
+
return layer(x)
|
| 718 |
+
|
| 719 |
+
def _bottleneck(
|
| 720 |
+
self,
|
| 721 |
+
x,
|
| 722 |
+
organ_id=None,
|
| 723 |
+
forward_ctx=None,
|
| 724 |
+
):
|
| 725 |
+
if isinstance(self.bottleneck, UpConvBlockFiLM):
|
| 726 |
+
if forward_ctx is not None and forward_ctx.get("mod_list") is not None:
|
| 727 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 728 |
+
return self.bottleneck(x, gammas=gammas, betas=betas)
|
| 729 |
+
return self.bottleneck(x, organ_id=organ_id)
|
| 730 |
+
else:
|
| 731 |
+
return self.bottleneck(x)
|
| 732 |
+
|
| 733 |
+
def __str__(self):
|
| 734 |
+
model_parameters = filter(lambda p: p.requires_grad, self.parameters())
|
| 735 |
+
params = sum([np.prod(p.size()) for p in model_parameters])
|
| 736 |
+
film_status = "enabled" if self.use_film else "disabled"
|
| 737 |
+
film_range = f"from level {self.film_start}" if self.use_film else "N/A"
|
| 738 |
+
return (
|
| 739 |
+
super().__str__() + f"\nTrainable parameters: {params}"
|
| 740 |
+
f"\nFiLM: {film_status} ({film_range})"
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
|
| 744 |
+
class DiceBCELoss(nn.Module):
|
| 745 |
+
def __init__(self, dice_weight: float = 1.0, bce_weight: float = 1.0):
|
| 746 |
+
super().__init__()
|
| 747 |
+
self.dice_weight = dice_weight
|
| 748 |
+
self.bce_weight = bce_weight
|
| 749 |
+
self.eps = 1e-6
|
| 750 |
+
|
| 751 |
+
def forward(self, logits: torch.Tensor, gt: torch.Tensor) -> torch.Tensor:
|
| 752 |
+
|
| 753 |
+
gt = gt.float()
|
| 754 |
+
|
| 755 |
+
bce = F.binary_cross_entropy_with_logits(
|
| 756 |
+
logits.squeeze(), gt.squeeze(), reduction="mean"
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
# Soft Dice loss
|
| 760 |
+
probs = torch.sigmoid(logits)
|
| 761 |
+
dims = tuple(range(2, probs.dim())) # (H, W) or (D,H,W)
|
| 762 |
+
|
| 763 |
+
# per‑class Dice, per‑sample
|
| 764 |
+
inter = (probs * gt).sum(dims) * 2
|
| 765 |
+
union = probs.sum(dims) + gt.sum(dims)
|
| 766 |
+
dice = 1 - (inter + self.eps) / (union + self.eps) # [B, C]
|
| 767 |
+
|
| 768 |
+
dice = dice.mean()
|
| 769 |
+
|
| 770 |
+
loss = self.dice_weight * dice + self.bce_weight * bce
|
| 771 |
+
return loss
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
class MedSAM(nn.Module):
|
| 775 |
+
def __init__(
|
| 776 |
+
self,
|
| 777 |
+
image_encoder,
|
| 778 |
+
mask_decoder,
|
| 779 |
+
prompt_encoder,
|
| 780 |
+
freeze_image_encoder=True,
|
| 781 |
+
predict_bboxes=False,
|
| 782 |
+
image_encoder_lora_rank=0,
|
| 783 |
+
):
|
| 784 |
+
super().__init__()
|
| 785 |
+
self.image_encoder = image_encoder
|
| 786 |
+
self.mask_decoder = mask_decoder
|
| 787 |
+
self.prompt_encoder = prompt_encoder
|
| 788 |
+
self.criterion = DiceBCELoss()
|
| 789 |
+
self.freeze_image_encoder = freeze_image_encoder
|
| 790 |
+
self.predict_bboxes = predict_bboxes
|
| 791 |
+
|
| 792 |
+
if self.freeze_image_encoder:
|
| 793 |
+
for param in self.image_encoder.parameters():
|
| 794 |
+
param.requires_grad = False
|
| 795 |
+
|
| 796 |
+
self.image_encoder_lora_rank = int(image_encoder_lora_rank)
|
| 797 |
+
self.image_encoder_lora_layers = 0
|
| 798 |
+
if self.image_encoder_lora_rank > 0:
|
| 799 |
+
self.image_encoder, self.image_encoder_lora_layers = apply_lora_to_qv_projections(
|
| 800 |
+
self.image_encoder,
|
| 801 |
+
rank=self.image_encoder_lora_rank,
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
# Classification head
|
| 805 |
+
self.multi_cls = nn.Sequential(nn.Linear(256 * 64 * 64, 10))
|
| 806 |
+
|
| 807 |
+
# Bounding box regression head
|
| 808 |
+
self.bbox_regr = nn.Sequential(nn.Linear(256 * 64 * 64, 4))
|
| 809 |
+
|
| 810 |
+
# Learnable prompt embeddings (no input required)
|
| 811 |
+
self.learned_sparse_embeddings = nn.Parameter(
|
| 812 |
+
torch.randn(1, 2, 256) # (batch, num_tokens, embed_dim)
|
| 813 |
+
)
|
| 814 |
+
self.learned_dense_embeddings = nn.Parameter(
|
| 815 |
+
torch.randn(1, 256, 64, 64) # (batch, embed_dim, H, W)
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
def forward(
|
| 819 |
+
self,
|
| 820 |
+
pixel_values,
|
| 821 |
+
organ_id=None,
|
| 822 |
+
labels=None,
|
| 823 |
+
masks=None,
|
| 824 |
+
bbox_coords=None,
|
| 825 |
+
organ_id_metric=None,
|
| 826 |
+
pixel_values_medsam=None,
|
| 827 |
+
**kwargs, # ignored, for peft compatibility
|
| 828 |
+
):
|
| 829 |
+
batch_size = pixel_values.shape[0]
|
| 830 |
+
|
| 831 |
+
# Get image embeddings
|
| 832 |
+
image_embedding = self.image_encoder(pixel_values_medsam) # (B, 256, 64, 64)
|
| 833 |
+
|
| 834 |
+
# Classification output
|
| 835 |
+
emb_flattened = torch.flatten(image_embedding, 1)
|
| 836 |
+
multi_cls_out = self.multi_cls(emb_flattened)
|
| 837 |
+
|
| 838 |
+
# Bounding box output (if enabled)
|
| 839 |
+
if self.predict_bboxes:
|
| 840 |
+
bbox_out = self.bbox_regr(emb_flattened)
|
| 841 |
+
else:
|
| 842 |
+
bbox_out = None
|
| 843 |
+
|
| 844 |
+
# Expand learned embeddings to batch size
|
| 845 |
+
sparse_embeddings = self.learned_sparse_embeddings
|
| 846 |
+
dense_embeddings = self.learned_dense_embeddings
|
| 847 |
+
|
| 848 |
+
# Get positional encoding
|
| 849 |
+
image_pe = self.prompt_encoder.get_dense_pe() # (1, 256, 64, 64)
|
| 850 |
+
|
| 851 |
+
# Decode mask
|
| 852 |
+
low_res_masks, iou_predictions = self.mask_decoder(
|
| 853 |
+
image_embeddings=image_embedding, # (B, 256, 64, 64)
|
| 854 |
+
image_pe=image_pe, # (1, 256, 64, 64)
|
| 855 |
+
sparse_prompt_embeddings=sparse_embeddings, # (B, 2, 256)
|
| 856 |
+
dense_prompt_embeddings=dense_embeddings, # (B, 256, 64, 64)
|
| 857 |
+
multimask_output=False,
|
| 858 |
+
) # (B, 1, 256, 256)
|
| 859 |
+
if masks is not None:
|
| 860 |
+
loss = self.criterion(
|
| 861 |
+
low_res_masks.squeeze(1), v2.functional.resize(masks, (256, 256))
|
| 862 |
+
)
|
| 863 |
+
else:
|
| 864 |
+
loss = 0.0
|
| 865 |
+
|
| 866 |
+
return {
|
| 867 |
+
"loss": loss,
|
| 868 |
+
"logits": low_res_masks.squeeze(1),
|
| 869 |
+
"labels": masks,
|
| 870 |
+
"organ_id": organ_id,
|
| 871 |
+
"organ_id_metric": organ_id_metric,
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
class DistillationLoss(nn.Module):
|
| 876 |
+
"""
|
| 877 |
+
Improved distillation loss with multiple components:
|
| 878 |
+
- Cosine similarity loss (direction alignment)
|
| 879 |
+
- MSE loss (magnitude alignment)
|
| 880 |
+
- Optional L1 loss (sparsity)
|
| 881 |
+
"""
|
| 882 |
+
|
| 883 |
+
def __init__(self, temperature=3.0, alpha=0.5, use_l1=False):
|
| 884 |
+
super().__init__()
|
| 885 |
+
self.temperature = temperature
|
| 886 |
+
self.alpha = alpha # Weight between cosine and MSE
|
| 887 |
+
self.use_l1 = use_l1
|
| 888 |
+
|
| 889 |
+
def forward(self, student_logits, teacher_logits, tau=0.7):
|
| 890 |
+
"""
|
| 891 |
+
Args:
|
| 892 |
+
student_logits: (B, D) - student features
|
| 893 |
+
teacher_logits: (B, D) - teacher features
|
| 894 |
+
"""
|
| 895 |
+
# # Normalize features for stable training
|
| 896 |
+
# student_norm = F.normalize(student_logits, p=2, dim=1)
|
| 897 |
+
# teacher_norm = F.normalize(teacher_logits, p=2, dim=1)
|
| 898 |
+
|
| 899 |
+
# # Cosine similarity loss (encourages directional alignment)
|
| 900 |
+
# cosine_loss = (
|
| 901 |
+
# 1 - F.cosine_similarity(student_logits, teacher_logits, dim=1).mean()
|
| 902 |
+
# )
|
| 903 |
+
|
| 904 |
+
# # MSE loss on normalized features (encourages magnitude alignment)
|
| 905 |
+
# mse_loss = F.mse_loss(student_norm, teacher_norm)
|
| 906 |
+
|
| 907 |
+
# # Combined loss
|
| 908 |
+
# loss = self.alpha * cosine_loss + (1 - self.alpha) * mse_loss
|
| 909 |
+
|
| 910 |
+
# # Optional L1 for sparsity
|
| 911 |
+
# if self.use_l1:
|
| 912 |
+
# l1_loss = F.l1_loss(student_logits, teacher_logits)
|
| 913 |
+
# loss = loss + 0.1 * l1_loss
|
| 914 |
+
|
| 915 |
+
B = student_logits.size(0)
|
| 916 |
+
|
| 917 |
+
s = student_logits.flatten(start_dim=1)
|
| 918 |
+
t = teacher_logits.flatten(start_dim=1).detach()
|
| 919 |
+
|
| 920 |
+
s = F.normalize(s, dim=-1)
|
| 921 |
+
t = F.normalize(t, dim=-1)
|
| 922 |
+
|
| 923 |
+
d2 = (s[:, None, :] - t[None, :, :]).pow(2).sum(dim=-1)
|
| 924 |
+
logits = -d2 / tau
|
| 925 |
+
labels = torch.arange(B, device=s.device)
|
| 926 |
+
|
| 927 |
+
loss = F.cross_entropy(logits, labels)
|
| 928 |
+
return {
|
| 929 |
+
"loss": loss,
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
class MedSAMPrompt(nn.Module):
|
| 934 |
+
def __init__(
|
| 935 |
+
self,
|
| 936 |
+
image_encoder,
|
| 937 |
+
mask_decoder,
|
| 938 |
+
prompt_encoder,
|
| 939 |
+
freeze_image_encoder=True,
|
| 940 |
+
predict_bboxes=False,
|
| 941 |
+
n_organs=1,
|
| 942 |
+
image_encoder_lora_rank=0,
|
| 943 |
+
):
|
| 944 |
+
super().__init__()
|
| 945 |
+
self.image_encoder = image_encoder
|
| 946 |
+
self.mask_decoder = mask_decoder
|
| 947 |
+
self.prompt_encoder = prompt_encoder
|
| 948 |
+
self.criterion = DiceBCELoss()
|
| 949 |
+
self.freeze_image_encoder = freeze_image_encoder
|
| 950 |
+
self.predict_bboxes = predict_bboxes
|
| 951 |
+
|
| 952 |
+
if self.freeze_image_encoder:
|
| 953 |
+
for param in self.image_encoder.parameters():
|
| 954 |
+
param.requires_grad = False
|
| 955 |
+
|
| 956 |
+
self.image_encoder_lora_rank = int(image_encoder_lora_rank)
|
| 957 |
+
self.image_encoder_lora_layers = 0
|
| 958 |
+
if self.image_encoder_lora_rank > 0:
|
| 959 |
+
self.image_encoder, self.image_encoder_lora_layers = apply_lora_to_qv_projections(
|
| 960 |
+
self.image_encoder,
|
| 961 |
+
rank=self.image_encoder_lora_rank,
|
| 962 |
+
)
|
| 963 |
+
|
| 964 |
+
# Classification head
|
| 965 |
+
self.multi_cls = nn.Sequential(nn.Linear(256 * 64 * 64, 10))
|
| 966 |
+
|
| 967 |
+
# Bounding box regression head
|
| 968 |
+
self.bbox_regr = nn.Sequential(nn.Linear(256 * 64 * 64, 4))
|
| 969 |
+
|
| 970 |
+
# Learnable prompt embeddings (no input required)
|
| 971 |
+
self.sparse_embeddings = nn.Parameter(torch.randn(1, 2, 256))
|
| 972 |
+
self.dense_embeddings = nn.Embedding(n_organs + 1, 1 * 256 * 64 * 64)
|
| 973 |
+
|
| 974 |
+
# self.sparse_embeddings = nn.ParameterDict(
|
| 975 |
+
# {
|
| 976 |
+
# str(id_): nn.Parameter(torch.randn(1, 2, 256))
|
| 977 |
+
# for id_ in set(organ_to_class_dict.values())
|
| 978 |
+
# }
|
| 979 |
+
# )
|
| 980 |
+
# self.dense_embeddings = nn.ParameterDict(
|
| 981 |
+
# {
|
| 982 |
+
# str(id_): nn.Parameter(torch.randn(1, 256, 64, 64))
|
| 983 |
+
# for id_ in set(organ_to_class_dict.values())
|
| 984 |
+
# }
|
| 985 |
+
# )
|
| 986 |
+
|
| 987 |
+
def forward(
|
| 988 |
+
self,
|
| 989 |
+
pixel_values,
|
| 990 |
+
organ_id=None,
|
| 991 |
+
labels=None,
|
| 992 |
+
masks=None,
|
| 993 |
+
bbox_coords=None,
|
| 994 |
+
organ_id_metric=None,
|
| 995 |
+
pixel_values_medsam=None,
|
| 996 |
+
**kwargs, # ignored, for peft compatibility
|
| 997 |
+
):
|
| 998 |
+
batch_size = pixel_values.shape[0]
|
| 999 |
+
B = pixel_values.shape[0]
|
| 1000 |
+
|
| 1001 |
+
# Get image embeddings
|
| 1002 |
+
image_embedding = self.image_encoder(pixel_values_medsam) # (B, 256, 64, 64)
|
| 1003 |
+
|
| 1004 |
+
# Classification output
|
| 1005 |
+
emb_flattened = torch.flatten(image_embedding, 1)
|
| 1006 |
+
multi_cls_out = self.multi_cls(emb_flattened)
|
| 1007 |
+
|
| 1008 |
+
# Bounding box output (if enabled)
|
| 1009 |
+
if self.predict_bboxes:
|
| 1010 |
+
bbox_out = self.bbox_regr(emb_flattened)
|
| 1011 |
+
else:
|
| 1012 |
+
bbox_out = None
|
| 1013 |
+
|
| 1014 |
+
if organ_id is None:
|
| 1015 |
+
raise ValueError("organ_id must be provided for selecting embeddings.")
|
| 1016 |
+
|
| 1017 |
+
sparse_emb_list, dense_emb_list = [], []
|
| 1018 |
+
|
| 1019 |
+
mask = organ_id >= 0 # [B]
|
| 1020 |
+
idx = torch.where(mask, organ_id, self.dense_embeddings.weight.shape[0] - 1)
|
| 1021 |
+
dense_embeddings = self.dense_embeddings(idx).view(B, 256, 64, 64)
|
| 1022 |
+
|
| 1023 |
+
# Get positional encoding
|
| 1024 |
+
image_pe = self.prompt_encoder.get_dense_pe() # (1, 256, 64, 64)
|
| 1025 |
+
|
| 1026 |
+
# Decode mask
|
| 1027 |
+
low_res_masks, iou_predictions = self.mask_decoder(
|
| 1028 |
+
image_embeddings=image_embedding, # (B, 256, 64, 64)
|
| 1029 |
+
image_pe=image_pe, # (1, 256, 64, 64)
|
| 1030 |
+
sparse_prompt_embeddings=self.sparse_embeddings, # (B, 2, 256)
|
| 1031 |
+
dense_prompt_embeddings=dense_embeddings, # (B, 256, 64, 64)
|
| 1032 |
+
multimask_output=False,
|
| 1033 |
+
) # (B, 1, 256, 256)
|
| 1034 |
+
if masks is not None:
|
| 1035 |
+
loss = self.criterion(
|
| 1036 |
+
low_res_masks.squeeze(1), v2.functional.resize(masks, (256, 256))
|
| 1037 |
+
)
|
| 1038 |
+
else:
|
| 1039 |
+
loss = 0.0
|
| 1040 |
+
|
| 1041 |
+
return {
|
| 1042 |
+
"loss": loss,
|
| 1043 |
+
"logits": low_res_masks.squeeze(1),
|
| 1044 |
+
"labels": masks,
|
| 1045 |
+
"organ_id": organ_id,
|
| 1046 |
+
"organ_id_metric": organ_id_metric,
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
|
| 1050 |
+
|
| 1051 |
+
# config = {"in_channels": 3,"num_classes": 1,"n_organs": 8,"size": 32,"depth": 5,"film_start": 0,"use_film": 1}
|
unet_attn.py
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch, wandb
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from einops import rearrange
|
| 5 |
+
from .segm_net import (
|
| 6 |
+
ConvBlock,
|
| 7 |
+
DownConvBlock,
|
| 8 |
+
UpConvBlock,
|
| 9 |
+
DiceBCELoss,
|
| 10 |
+
)
|
| 11 |
+
import ptwt
|
| 12 |
+
import numpy as np
|
| 13 |
+
from .unet_base import BaseUnet
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def canonicalize_mask_batch(gt_masks: torch.Tensor, canon_res: int = 32, pad: int = 4):
|
| 17 |
+
"""
|
| 18 |
+
Args:
|
| 19 |
+
gt_masks: (B, H, W) binary float tensor (0/1)
|
| 20 |
+
canon_res: output resolution (int)
|
| 21 |
+
pad: pixels of padding around bbox (optional)
|
| 22 |
+
Returns:
|
| 23 |
+
canon_masks: (B, canon_res, canon_res) float tensor in [0,1]
|
| 24 |
+
"""
|
| 25 |
+
B, H, W = gt_masks.shape
|
| 26 |
+
device = gt_masks.device
|
| 27 |
+
canon_masks = torch.zeros(
|
| 28 |
+
(B, canon_res, canon_res), device=device, dtype=gt_masks.dtype
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
for i in range(B):
|
| 32 |
+
m = gt_masks[i]
|
| 33 |
+
nz = torch.nonzero(m, as_tuple=False)
|
| 34 |
+
if nz.numel() == 0:
|
| 35 |
+
# empty mask -> keep zeros
|
| 36 |
+
continue
|
| 37 |
+
y_min = int(nz[:, 0].min().clamp(0, H - 1).item())
|
| 38 |
+
y_max = int(nz[:, 0].max().clamp(0, H - 1).item())
|
| 39 |
+
x_min = int(nz[:, 1].min().clamp(0, W - 1).item())
|
| 40 |
+
x_max = int(nz[:, 1].max().clamp(0, W - 1).item())
|
| 41 |
+
|
| 42 |
+
# pad bbox
|
| 43 |
+
y0 = max(0, y_min - pad)
|
| 44 |
+
y1 = min(H, y_max + pad + 1)
|
| 45 |
+
x0 = max(0, x_min - pad)
|
| 46 |
+
x1 = min(W, x_max + pad + 1)
|
| 47 |
+
|
| 48 |
+
crop = m[y0:y1, x0:x1].unsqueeze(0).unsqueeze(0) # (1,1,hc,wc)
|
| 49 |
+
# Resize to canonical resolution
|
| 50 |
+
crop_resized = F.interpolate(
|
| 51 |
+
crop, size=(canon_res, canon_res), mode="bilinear", align_corners=False
|
| 52 |
+
)
|
| 53 |
+
canon_masks[i] = crop_resized[0, 0]
|
| 54 |
+
|
| 55 |
+
return canon_masks # (B, canon_res, canon_res)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def canonicalize_mask_batch_normalized(
|
| 59 |
+
gt_masks: torch.Tensor,
|
| 60 |
+
canon_res: int = 32,
|
| 61 |
+
target_scale: float = 0.7, # Target mask to fill 70% of canonical space
|
| 62 |
+
):
|
| 63 |
+
"""
|
| 64 |
+
Canonical masks with consistent scale normalization.
|
| 65 |
+
"""
|
| 66 |
+
B, H, W = gt_masks.shape
|
| 67 |
+
device = gt_masks.device
|
| 68 |
+
canon_masks = torch.zeros(
|
| 69 |
+
(B, canon_res, canon_res), device=device, dtype=gt_masks.dtype
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
for i in range(B):
|
| 73 |
+
m = gt_masks[i]
|
| 74 |
+
nz = torch.nonzero(m, as_tuple=False)
|
| 75 |
+
if nz.numel() == 0:
|
| 76 |
+
continue
|
| 77 |
+
|
| 78 |
+
y_coords = nz[:, 0].float()
|
| 79 |
+
x_coords = nz[:, 1].float()
|
| 80 |
+
|
| 81 |
+
# Center of mass
|
| 82 |
+
com_y = y_coords.mean()
|
| 83 |
+
com_x = x_coords.mean()
|
| 84 |
+
|
| 85 |
+
# Bbox dimensions
|
| 86 |
+
y_min, y_max = y_coords.min(), y_coords.max()
|
| 87 |
+
x_min, x_max = x_coords.min(), x_coords.max()
|
| 88 |
+
bbox_h = (y_max - y_min + 1).item()
|
| 89 |
+
bbox_w = (x_max - x_min + 1).item()
|
| 90 |
+
|
| 91 |
+
# Compute crop size to achieve target scale
|
| 92 |
+
max_dim = max(bbox_h, bbox_w)
|
| 93 |
+
crop_size = int(
|
| 94 |
+
max_dim / target_scale
|
| 95 |
+
) # Scale up to make mask fill target_scale
|
| 96 |
+
|
| 97 |
+
# Create square crop centered on COM
|
| 98 |
+
half_crop = crop_size // 2
|
| 99 |
+
y0 = int(com_y.item()) - half_crop
|
| 100 |
+
y1 = y0 + crop_size
|
| 101 |
+
x0 = int(com_x.item()) - half_crop
|
| 102 |
+
x1 = x0 + crop_size
|
| 103 |
+
|
| 104 |
+
# Clamp and pad
|
| 105 |
+
y0_clamped = max(0, y0)
|
| 106 |
+
y1_clamped = min(H, y1)
|
| 107 |
+
x0_clamped = max(0, x0)
|
| 108 |
+
x1_clamped = min(W, x1)
|
| 109 |
+
|
| 110 |
+
crop = m[y0_clamped:y1_clamped, x0_clamped:x1_clamped]
|
| 111 |
+
|
| 112 |
+
# Padding
|
| 113 |
+
pad_top = y0_clamped - y0
|
| 114 |
+
pad_bottom = crop_size - (y1_clamped - y0_clamped) - pad_top
|
| 115 |
+
pad_left = x0_clamped - x0
|
| 116 |
+
pad_right = crop_size - (x1_clamped - x0_clamped) - pad_left
|
| 117 |
+
|
| 118 |
+
crop = F.pad(
|
| 119 |
+
crop.unsqueeze(0).unsqueeze(0),
|
| 120 |
+
(pad_left, pad_right, pad_top, pad_bottom),
|
| 121 |
+
mode="constant",
|
| 122 |
+
value=0,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Resize
|
| 126 |
+
crop_resized = F.interpolate(
|
| 127 |
+
crop, size=(canon_res, canon_res), mode="bilinear", align_corners=False
|
| 128 |
+
)
|
| 129 |
+
canon_masks[i] = crop_resized[0, 0]
|
| 130 |
+
|
| 131 |
+
return canon_masks
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class PatchEmbed(nn.Module):
|
| 135 |
+
"""Convert image to patches and embed them with positional encoding"""
|
| 136 |
+
|
| 137 |
+
def __init__(self, img_size=256, patch_size=16, in_channels=3, embed_dim=256):
|
| 138 |
+
super().__init__()
|
| 139 |
+
self.img_size = img_size
|
| 140 |
+
self.patch_size = patch_size
|
| 141 |
+
self.n_patches = (img_size // patch_size) ** 2
|
| 142 |
+
|
| 143 |
+
self.proj = nn.Conv2d(
|
| 144 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# Positional encoding - CRITICAL for spatial understanding
|
| 148 |
+
self.pos_embed = nn.Parameter(torch.zeros(1, self.n_patches, embed_dim))
|
| 149 |
+
nn.init.trunc_normal_(self.pos_embed, std=0.02)
|
| 150 |
+
|
| 151 |
+
def forward(self, x):
|
| 152 |
+
"""
|
| 153 |
+
x: (B, C, H, W)
|
| 154 |
+
returns: (B, N, D) where N = num_patches
|
| 155 |
+
"""
|
| 156 |
+
x = self.proj(x) # (B, embed_dim, H/P, W/P)
|
| 157 |
+
x = rearrange(x, "b c h w -> b (h w) c") # (B, N, D)
|
| 158 |
+
x = x + self.pos_embed # Add positional encoding
|
| 159 |
+
return x
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class DWTPatchEmbed(nn.Module):
|
| 163 |
+
"""
|
| 164 |
+
Apply DWT and create patch embeddings from subbands.
|
| 165 |
+
Interface matches PatchEmbed to minimize changes to SharedAttnModulator.
|
| 166 |
+
|
| 167 |
+
DWT decomposes (B, C, H, W) into:
|
| 168 |
+
- LL: Low-freq approximation (B, C, H/2, W/2)
|
| 169 |
+
- LH, HL, HH: High-freq details (B, C, H/2, W/2 each)
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
def __init__(
|
| 173 |
+
self,
|
| 174 |
+
img_size=256,
|
| 175 |
+
patch_size=8,
|
| 176 |
+
in_channels=3,
|
| 177 |
+
embed_dim=256,
|
| 178 |
+
wavelet="haar",
|
| 179 |
+
mode="zero",
|
| 180 |
+
bands: list[str] | None = None,
|
| 181 |
+
):
|
| 182 |
+
super().__init__()
|
| 183 |
+
self.img_size = img_size
|
| 184 |
+
self.patch_size = patch_size
|
| 185 |
+
self.wavelet = wavelet
|
| 186 |
+
self.mode = mode
|
| 187 |
+
self.available_bands = ["LL", "LH", "HL", "HH"]
|
| 188 |
+
if not bands:
|
| 189 |
+
self.bands = list(self.available_bands)
|
| 190 |
+
else:
|
| 191 |
+
normalized = []
|
| 192 |
+
seen = set()
|
| 193 |
+
for band in bands:
|
| 194 |
+
band = band.upper()
|
| 195 |
+
if band in seen:
|
| 196 |
+
continue
|
| 197 |
+
if band not in self.available_bands:
|
| 198 |
+
raise ValueError(
|
| 199 |
+
f"Invalid DWT band '{band}'. Valid bands: {self.available_bands}"
|
| 200 |
+
)
|
| 201 |
+
normalized.append(band)
|
| 202 |
+
seen.add(band)
|
| 203 |
+
self.bands = normalized
|
| 204 |
+
|
| 205 |
+
# After 1-level DWT, each subband is img_size/2
|
| 206 |
+
dwt_size = img_size // 2 # 128 for img_size=256
|
| 207 |
+
|
| 208 |
+
# Number of patches per subband
|
| 209 |
+
patches_per_dim = dwt_size // patch_size # 128/8 = 16
|
| 210 |
+
n_patches_per_band = patches_per_dim**2 # 256
|
| 211 |
+
|
| 212 |
+
# Total patches: selected subbands × n_patches_per_band
|
| 213 |
+
self.n_patches = len(self.bands) * n_patches_per_band
|
| 214 |
+
self.n_patches_per_band = n_patches_per_band
|
| 215 |
+
|
| 216 |
+
# Separate projections for each subband
|
| 217 |
+
self.proj_LL = nn.Conv2d(
|
| 218 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 219 |
+
)
|
| 220 |
+
self.proj_LH = nn.Conv2d(
|
| 221 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 222 |
+
)
|
| 223 |
+
self.proj_HL = nn.Conv2d(
|
| 224 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 225 |
+
)
|
| 226 |
+
self.proj_HH = nn.Conv2d(
|
| 227 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Shared positional encoding for all patches
|
| 231 |
+
self.pos_embed = nn.Parameter(torch.zeros(1, self.n_patches, embed_dim))
|
| 232 |
+
nn.init.trunc_normal_(self.pos_embed, std=0.02)
|
| 233 |
+
|
| 234 |
+
# Learnable subband type embeddings
|
| 235 |
+
self.subband_type_embed = nn.Parameter(torch.zeros(4, embed_dim))
|
| 236 |
+
nn.init.trunc_normal_(self.subband_type_embed, std=0.02)
|
| 237 |
+
|
| 238 |
+
def forward(self, x):
|
| 239 |
+
"""
|
| 240 |
+
Args:
|
| 241 |
+
x: (B, C, H, W) - input image
|
| 242 |
+
Returns:
|
| 243 |
+
patches: (B, N, D) where N = len(bands) * (H/2/patch_size)^2
|
| 244 |
+
"""
|
| 245 |
+
B, C, H, W = x.shape
|
| 246 |
+
|
| 247 |
+
# Apply DWT using ptwt - correct API
|
| 248 |
+
# wavedec2 returns coefficients in format: [LL, (LH, HL, HH)]
|
| 249 |
+
coeffs = ptwt.wavedec2(x, wavelet=self.wavelet, mode=self.mode, level=1)
|
| 250 |
+
|
| 251 |
+
# Extract subbands
|
| 252 |
+
# coeffs[0] is LL (approximation)
|
| 253 |
+
# coeffs[1] is tuple of (LH, HL, HH) detail coefficients
|
| 254 |
+
LL = coeffs[0] # (B, C, H/2, W/2)
|
| 255 |
+
LH, HL, HH = coeffs[1] # Each (B, C, H/2, W/2)
|
| 256 |
+
|
| 257 |
+
subband_tensors = {
|
| 258 |
+
"LL": (LL, self.proj_LL, 0),
|
| 259 |
+
"LH": (LH, self.proj_LH, 1),
|
| 260 |
+
"HL": (HL, self.proj_HL, 2),
|
| 261 |
+
"HH": (HH, self.proj_HH, 3),
|
| 262 |
+
}
|
| 263 |
+
patches_list = []
|
| 264 |
+
for band in self.bands:
|
| 265 |
+
band_tensor, proj, band_idx = subband_tensors[band]
|
| 266 |
+
band_patches = proj(band_tensor) # (B, embed_dim, H/2/P, W/2/P)
|
| 267 |
+
band_patches = rearrange(band_patches, "b d h w -> b (h w) d")
|
| 268 |
+
band_patches = band_patches + self.subband_type_embed[band_idx]
|
| 269 |
+
patches_list.append(band_patches)
|
| 270 |
+
|
| 271 |
+
# Concatenate selected subbands
|
| 272 |
+
patches = torch.cat(patches_list, dim=1)
|
| 273 |
+
|
| 274 |
+
# Add positional encoding
|
| 275 |
+
patches = patches + self.pos_embed
|
| 276 |
+
|
| 277 |
+
return patches
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class FiLMLayer(nn.Module):
|
| 281 |
+
"""Simple FiLM layer that applies gamma * x + beta modulation"""
|
| 282 |
+
|
| 283 |
+
def __init__(self):
|
| 284 |
+
super().__init__()
|
| 285 |
+
|
| 286 |
+
def forward(self, x: torch.Tensor, gamma: torch.Tensor, beta: torch.Tensor):
|
| 287 |
+
"""
|
| 288 |
+
Args:
|
| 289 |
+
x: (B, C, H, W) - input features
|
| 290 |
+
gamma: (B, C, 1, 1) - scaling factors
|
| 291 |
+
beta: (B, C, 1, 1) - bias terms
|
| 292 |
+
Returns:
|
| 293 |
+
(B, C, H, W) - modulated features
|
| 294 |
+
"""
|
| 295 |
+
return gamma * x + beta
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
class SharedAttnModulator(nn.Module):
|
| 299 |
+
"""
|
| 300 |
+
Shared attention modulator that computes gamma/beta for all layers at once.
|
| 301 |
+
Uses layer_id embeddings to differentiate between layers.
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
def __init__(
|
| 305 |
+
self,
|
| 306 |
+
n_organs: int,
|
| 307 |
+
n_layers: int,
|
| 308 |
+
max_channels: int,
|
| 309 |
+
img_size: int = 256,
|
| 310 |
+
patch_size: int = 16,
|
| 311 |
+
img_channels: int = 3,
|
| 312 |
+
emb_dim: int = 256,
|
| 313 |
+
n_heads: int = 8,
|
| 314 |
+
dropout: float = 0.1,
|
| 315 |
+
use_dwt: bool = True,
|
| 316 |
+
wavelet: str = "haar",
|
| 317 |
+
dwt_bands: list[str] | None = None,
|
| 318 |
+
use_shape: bool = False,
|
| 319 |
+
shape_res=32,
|
| 320 |
+
) -> None:
|
| 321 |
+
super().__init__()
|
| 322 |
+
self.emb_dim = emb_dim
|
| 323 |
+
self.n_layers = n_layers
|
| 324 |
+
self.max_channels = max_channels
|
| 325 |
+
self.use_shape = use_shape
|
| 326 |
+
self.shape_res = shape_res
|
| 327 |
+
|
| 328 |
+
if use_dwt:
|
| 329 |
+
self.patch_embed = DWTPatchEmbed(
|
| 330 |
+
img_size=img_size,
|
| 331 |
+
patch_size=patch_size,
|
| 332 |
+
in_channels=img_channels,
|
| 333 |
+
embed_dim=emb_dim,
|
| 334 |
+
wavelet=wavelet,
|
| 335 |
+
bands=dwt_bands,
|
| 336 |
+
)
|
| 337 |
+
else:
|
| 338 |
+
self.patch_embed = PatchEmbed(
|
| 339 |
+
img_size=img_size,
|
| 340 |
+
patch_size=patch_size,
|
| 341 |
+
in_channels=img_channels,
|
| 342 |
+
embed_dim=emb_dim,
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# Organ embedding
|
| 346 |
+
self.organ_embed = nn.Embedding(
|
| 347 |
+
n_organs + 1, emb_dim
|
| 348 |
+
) # the +1 is for the unknown organ
|
| 349 |
+
nn.init.normal_(self.organ_embed.weight, mean=0, std=0.02)
|
| 350 |
+
|
| 351 |
+
# Layer embedding
|
| 352 |
+
self.layer_embed = nn.Embedding(n_layers, emb_dim)
|
| 353 |
+
nn.init.normal_(self.layer_embed.weight, mean=0, std=0.02)
|
| 354 |
+
|
| 355 |
+
if self.use_shape:
|
| 356 |
+
# Shape embedding
|
| 357 |
+
self.shape_embed = nn.Embedding(
|
| 358 |
+
n_organs + 1, emb_dim
|
| 359 |
+
) # the +1 is for the unknown organ
|
| 360 |
+
nn.init.normal_(self.shape_embed.weight, mean=0, std=0.02)
|
| 361 |
+
self.shape_proj = nn.Linear(self.emb_dim, self.shape_res**2)
|
| 362 |
+
|
| 363 |
+
# Learnable influence token
|
| 364 |
+
self.influence_token = nn.Parameter(torch.zeros(1, 1, emb_dim))
|
| 365 |
+
nn.init.trunc_normal_(self.influence_token, std=0.02)
|
| 366 |
+
|
| 367 |
+
# Attention with dropout
|
| 368 |
+
self.attn = nn.MultiheadAttention(
|
| 369 |
+
embed_dim=emb_dim, num_heads=n_heads, dropout=dropout, batch_first=True
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
# Shared components for all layers
|
| 373 |
+
self.shared_norm = nn.LayerNorm(emb_dim)
|
| 374 |
+
|
| 375 |
+
# Per-layer linear projections
|
| 376 |
+
self.layer_linears = nn.ModuleList(
|
| 377 |
+
[nn.Linear(emb_dim, emb_dim) for _ in range(n_layers)]
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# Shared components after layer-specific linear
|
| 381 |
+
self.shared_gelu = nn.GELU()
|
| 382 |
+
self.shared_dropout = nn.Dropout(dropout)
|
| 383 |
+
self.shared_output = nn.Linear(emb_dim, 2 * max_channels)
|
| 384 |
+
|
| 385 |
+
# Initialize to identity (gamma≈1, beta≈0)
|
| 386 |
+
nn.init.zeros_(self.shared_output.weight)
|
| 387 |
+
nn.init.constant_(self.shared_output.bias[:max_channels], 0) # β
|
| 388 |
+
nn.init.constant_(self.shared_output.bias[max_channels:], 1) # γ
|
| 389 |
+
|
| 390 |
+
def compute_all_modulations(
|
| 391 |
+
self,
|
| 392 |
+
original_img: torch.Tensor,
|
| 393 |
+
organ_id: torch.Tensor,
|
| 394 |
+
layer_configs: list[tuple[int, int]],
|
| 395 |
+
return_compact: bool = False,
|
| 396 |
+
):
|
| 397 |
+
"""
|
| 398 |
+
Compute gamma and beta for all layers at once.
|
| 399 |
+
|
| 400 |
+
Args:
|
| 401 |
+
original_img: (B, 3, H, W) - original input image
|
| 402 |
+
organ_id: (B,) - organ type IDs (if >= 0, use organ embedding)
|
| 403 |
+
layer_configs: list of (layer_id, n_channels) tuples
|
| 404 |
+
|
| 405 |
+
Returns:
|
| 406 |
+
dict: {layer_id: (gamma, beta)} where gamma/beta are (B, C, 1, 1)
|
| 407 |
+
If return_compact=True, also returns raw (gamma_max, beta_max) per layer
|
| 408 |
+
before channel pooling, each shaped (B, max_channels).
|
| 409 |
+
|
| 410 |
+
Performance note: all L layer queries are stacked into a single attention
|
| 411 |
+
call (B, L*n_q, D) so the (B, N, D) key/value projections are computed
|
| 412 |
+
only once instead of L times. For N=4096, D=768, L=22 this is ~20x faster.
|
| 413 |
+
"""
|
| 414 |
+
B = original_img.shape[0]
|
| 415 |
+
L = len(layer_configs)
|
| 416 |
+
D = self.emb_dim
|
| 417 |
+
device = original_img.device
|
| 418 |
+
|
| 419 |
+
# Patch embedding — shared across all layers, computed once.
|
| 420 |
+
patches = self.patch_embed(original_img) # (B, N, D)
|
| 421 |
+
|
| 422 |
+
# Organ query base — same for all layers, select by organ_id validity.
|
| 423 |
+
mask = organ_id >= 0 # (B,)
|
| 424 |
+
unk_idx = torch.tensor(
|
| 425 |
+
[self.organ_embed.weight.shape[0] - 1], device=device
|
| 426 |
+
).expand(B)
|
| 427 |
+
q_org_base = torch.where(
|
| 428 |
+
mask[:, None],
|
| 429 |
+
self.organ_embed(organ_id.clamp(min=0)),
|
| 430 |
+
self.organ_embed(unk_idx),
|
| 431 |
+
) # (B, D)
|
| 432 |
+
|
| 433 |
+
# All layer embeddings at once.
|
| 434 |
+
all_layer_ids = torch.tensor(
|
| 435 |
+
[lid for lid, _ in layer_configs], device=device
|
| 436 |
+
) # (L,)
|
| 437 |
+
all_layer_embs = self.layer_embed(all_layer_ids) # (L, D)
|
| 438 |
+
|
| 439 |
+
# Per-layer organ query: q_org_base + layer_emb → (B, L, D)
|
| 440 |
+
q_per_layer = q_org_base.unsqueeze(1) + all_layer_embs.unsqueeze(0)
|
| 441 |
+
|
| 442 |
+
# Influence token tiled to (B, L, D).
|
| 443 |
+
influence_all = self.influence_token.expand(B, L, -1) # (B, L, D)
|
| 444 |
+
|
| 445 |
+
if self.use_shape:
|
| 446 |
+
unk_shape_idx = torch.tensor(
|
| 447 |
+
[self.shape_embed.weight.shape[0] - 1], device=device
|
| 448 |
+
).expand(B)
|
| 449 |
+
q_shape = torch.where(
|
| 450 |
+
mask[:, None],
|
| 451 |
+
self.shape_embed(organ_id.clamp(min=0)),
|
| 452 |
+
self.shape_embed(unk_shape_idx),
|
| 453 |
+
) # (B, D) — keep un-detached for shape_proj gradient
|
| 454 |
+
|
| 455 |
+
# projected_shape is identical for every layer (depends only on organ).
|
| 456 |
+
flattened_shape = self.shape_proj(q_shape) # (B, shape_res²)
|
| 457 |
+
projected_shape = flattened_shape.reshape(B, self.shape_res, self.shape_res)
|
| 458 |
+
projected_shapes = torch.stack([projected_shape] * L) # (L, B, sr, sr)
|
| 459 |
+
|
| 460 |
+
# Detach for use as attention query to isolate gradient path.
|
| 461 |
+
q_shape_attn = q_shape.detach().unsqueeze(1).expand(-1, L, -1) # (B, L, D)
|
| 462 |
+
|
| 463 |
+
# Stack: [influence, organ+layer, shape] → (B, L, 3, D)
|
| 464 |
+
queries_all = torch.stack(
|
| 465 |
+
[influence_all, q_per_layer, q_shape_attn], dim=2
|
| 466 |
+
)
|
| 467 |
+
n_q = 3
|
| 468 |
+
else:
|
| 469 |
+
projected_shapes = torch.Tensor([])
|
| 470 |
+
# Stack: [influence, organ+layer] → (B, L, 2, D)
|
| 471 |
+
queries_all = torch.stack([influence_all, q_per_layer], dim=2)
|
| 472 |
+
n_q = 2
|
| 473 |
+
|
| 474 |
+
# Single attention call: K/V projections on (B, N, D) happen only once
|
| 475 |
+
# instead of L times — the dominant cost for large N.
|
| 476 |
+
queries_flat = queries_all.reshape(B, L * n_q, D) # (B, L*n_q, D)
|
| 477 |
+
attn_out_flat, _ = self.attn(
|
| 478 |
+
query=queries_flat, # (B, L*n_q, D)
|
| 479 |
+
key=patches, # (B, N, D)
|
| 480 |
+
value=patches,
|
| 481 |
+
) # → (B, L*n_q, D)
|
| 482 |
+
|
| 483 |
+
# Mean-pool each layer's n_q tokens → (B, L, D)
|
| 484 |
+
attn_out_all = attn_out_flat.reshape(B, L, n_q, D).mean(dim=2)
|
| 485 |
+
|
| 486 |
+
# Per-layer post-processing (cheap: norm + linear + gelu + dropout + output).
|
| 487 |
+
modulations = {}
|
| 488 |
+
compact_modulations = {} if return_compact else None
|
| 489 |
+
for li, (layer_id, n_channels) in enumerate(layer_configs):
|
| 490 |
+
attn_out = attn_out_all[:, li, :] # (B, D)
|
| 491 |
+
|
| 492 |
+
x = self.shared_norm(attn_out)
|
| 493 |
+
x = self.layer_linears[layer_id](x)
|
| 494 |
+
x = self.shared_gelu(x)
|
| 495 |
+
x = self.shared_dropout(x)
|
| 496 |
+
gamma_beta = self.shared_output(x) # (B, 2 * max_channels)
|
| 497 |
+
|
| 498 |
+
beta_max, gamma_max = gamma_beta.chunk(2, dim=-1) # each (B, max_channels)
|
| 499 |
+
|
| 500 |
+
if return_compact:
|
| 501 |
+
compact_modulations[layer_id] = (gamma_max, beta_max, attn_out)
|
| 502 |
+
|
| 503 |
+
beta = F.adaptive_avg_pool1d(beta_max, n_channels).unsqueeze(-1).unsqueeze(-1)
|
| 504 |
+
gamma = F.adaptive_avg_pool1d(gamma_max, n_channels).unsqueeze(-1).unsqueeze(-1)
|
| 505 |
+
|
| 506 |
+
modulations[layer_id] = (gamma, beta)
|
| 507 |
+
|
| 508 |
+
if return_compact:
|
| 509 |
+
return modulations, projected_shapes.to(device), compact_modulations
|
| 510 |
+
return modulations, projected_shapes.to(device)
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
class DownConvBlockFiLM(nn.Module):
|
| 514 |
+
"""
|
| 515 |
+
Encoder block with FiLM modulation.
|
| 516 |
+
Conv → FiLM → Conv → FiLM → Pool
|
| 517 |
+
"""
|
| 518 |
+
|
| 519 |
+
def __init__(
|
| 520 |
+
self,
|
| 521 |
+
in_channels: list[int],
|
| 522 |
+
out_channels: list[int],
|
| 523 |
+
conv_kwargs={"kernel_size": 3, "stride": 1, "padding": 1},
|
| 524 |
+
):
|
| 525 |
+
super().__init__()
|
| 526 |
+
assert len(in_channels) == len(out_channels)
|
| 527 |
+
|
| 528 |
+
self.conv_blocks = nn.ModuleList(
|
| 529 |
+
[
|
| 530 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 531 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 532 |
+
]
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
self.film_layers = nn.ModuleList([FiLMLayer() for _ in out_channels])
|
| 536 |
+
|
| 537 |
+
self.pool = nn.MaxPool2d(2, 2)
|
| 538 |
+
|
| 539 |
+
def forward(self, x: torch.Tensor, gammas: list, betas: list):
|
| 540 |
+
"""
|
| 541 |
+
Args:
|
| 542 |
+
x: (B, C, H, W) - features from previous layer
|
| 543 |
+
gammas: list of gamma tensors for each conv block
|
| 544 |
+
betas: list of beta tensors for each conv block
|
| 545 |
+
"""
|
| 546 |
+
for conv, film, gamma, beta in zip(
|
| 547 |
+
self.conv_blocks, self.film_layers, gammas, betas
|
| 548 |
+
):
|
| 549 |
+
x = conv(x)
|
| 550 |
+
x = film(x, gamma, beta)
|
| 551 |
+
return self.pool(x), x
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
class UpConvBlockFiLM(nn.Module):
|
| 555 |
+
"""
|
| 556 |
+
Decoder block with FiLM modulation.
|
| 557 |
+
Conv → FiLM → Conv → FiLM → (optional) ConvTranspose2d
|
| 558 |
+
"""
|
| 559 |
+
|
| 560 |
+
def __init__(
|
| 561 |
+
self,
|
| 562 |
+
in_channels: list[int],
|
| 563 |
+
out_channels: list[int],
|
| 564 |
+
up_conv: bool = True,
|
| 565 |
+
conv_kwargs: dict = {"kernel_size": 3, "stride": 1, "padding": 1},
|
| 566 |
+
upconv_kwargs: dict = {"kernel_size": 2, "stride": 2},
|
| 567 |
+
):
|
| 568 |
+
super().__init__()
|
| 569 |
+
assert len(in_channels) == len(out_channels)
|
| 570 |
+
|
| 571 |
+
self.conv_blocks = nn.ModuleList(
|
| 572 |
+
[
|
| 573 |
+
ConvBlock(in_ch, out_ch, conv_kwargs)
|
| 574 |
+
for in_ch, out_ch in zip(in_channels, out_channels)
|
| 575 |
+
]
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
self.film_layers = nn.ModuleList([FiLMLayer() for _ in out_channels])
|
| 579 |
+
|
| 580 |
+
self.up_conv = up_conv
|
| 581 |
+
if self.up_conv:
|
| 582 |
+
self.up_conv_op = nn.ConvTranspose2d(
|
| 583 |
+
out_channels[-1], out_channels[-1], **upconv_kwargs
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
def forward(self, x: torch.Tensor, gammas: list, betas: list):
|
| 587 |
+
"""
|
| 588 |
+
Args:
|
| 589 |
+
x: (B, C, H, W)
|
| 590 |
+
gammas: list of gamma tensors for each conv block
|
| 591 |
+
betas: list of beta tensors for each conv block
|
| 592 |
+
"""
|
| 593 |
+
for conv, film, gamma, beta in zip(
|
| 594 |
+
self.conv_blocks, self.film_layers, gammas, betas
|
| 595 |
+
):
|
| 596 |
+
x = conv(x)
|
| 597 |
+
x = film(x, gamma, beta)
|
| 598 |
+
|
| 599 |
+
if self.up_conv:
|
| 600 |
+
x = self.up_conv_op(x)
|
| 601 |
+
|
| 602 |
+
return x
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
class UNet2DAttn(BaseUnet):
|
| 606 |
+
"""
|
| 607 |
+
UNet with shared attention-based conditioning.
|
| 608 |
+
Computes all gamma/beta at the start of forward pass.
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
def __init__(
|
| 612 |
+
self,
|
| 613 |
+
in_channels: int,
|
| 614 |
+
num_classes: int,
|
| 615 |
+
n_organs: int,
|
| 616 |
+
size: int = 32,
|
| 617 |
+
depth: int = 3,
|
| 618 |
+
*,
|
| 619 |
+
attn_start: int = 0,
|
| 620 |
+
use_attn: bool = True,
|
| 621 |
+
img_size: int = 512,
|
| 622 |
+
patch_size: int = 8,
|
| 623 |
+
emb_dim: int = 768,
|
| 624 |
+
n_heads: int = 8,
|
| 625 |
+
distill: bool = False,
|
| 626 |
+
distill_unet: bool = False,
|
| 627 |
+
medsam_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/medsam_unfreezed/model.safetensors",
|
| 628 |
+
unet_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/unet5_attn/model.safetensors",
|
| 629 |
+
unet_teacher_kwargs: dict | None = None,
|
| 630 |
+
use_dwt: bool = True,
|
| 631 |
+
wavelet: str = "haar",
|
| 632 |
+
dwt_bands: list[str] | None = None,
|
| 633 |
+
use_shape: bool = False,
|
| 634 |
+
shape_res: int = 64,
|
| 635 |
+
):
|
| 636 |
+
"""
|
| 637 |
+
Args:
|
| 638 |
+
in_channels: Number of input channels
|
| 639 |
+
num_classes: Number of output classes
|
| 640 |
+
n_organs: Number of organ types
|
| 641 |
+
size: Base number of channels
|
| 642 |
+
depth: Number of encoder/decoder levels
|
| 643 |
+
attn_start: Level where attention starts (0-based)
|
| 644 |
+
use_attn: If False, use plain Conv blocks
|
| 645 |
+
img_size: Size of input images (assumed square)
|
| 646 |
+
patch_size: Patch size for attention
|
| 647 |
+
emb_dim: Embedding dimension for attention
|
| 648 |
+
n_heads: Number of attention heads
|
| 649 |
+
"""
|
| 650 |
+
super().__init__(
|
| 651 |
+
in_channels=in_channels,
|
| 652 |
+
num_classes=num_classes,
|
| 653 |
+
n_organs=n_organs,
|
| 654 |
+
size=size,
|
| 655 |
+
depth=depth,
|
| 656 |
+
attn_start=attn_start,
|
| 657 |
+
use_attn=use_attn,
|
| 658 |
+
img_size=img_size,
|
| 659 |
+
patch_size=patch_size,
|
| 660 |
+
emb_dim=emb_dim,
|
| 661 |
+
n_heads=n_heads,
|
| 662 |
+
distill=distill,
|
| 663 |
+
distill_unet=distill_unet,
|
| 664 |
+
medsam_teacher_ckpt=medsam_teacher_ckpt,
|
| 665 |
+
unet_teacher_ckpt=unet_teacher_ckpt,
|
| 666 |
+
unet_teacher_kwargs=unet_teacher_kwargs,
|
| 667 |
+
use_dwt=use_dwt,
|
| 668 |
+
wavelet=wavelet,
|
| 669 |
+
dwt_bands=dwt_bands,
|
| 670 |
+
use_shape=use_shape,
|
| 671 |
+
shape_res=shape_res,
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
def _build_model(
|
| 675 |
+
self,
|
| 676 |
+
*,
|
| 677 |
+
attn_start: int = 0,
|
| 678 |
+
use_attn: bool = True,
|
| 679 |
+
img_size: int = 512,
|
| 680 |
+
patch_size: int = 8,
|
| 681 |
+
emb_dim: int = 768,
|
| 682 |
+
n_heads: int = 8,
|
| 683 |
+
distill: bool = False,
|
| 684 |
+
distill_unet: bool = False,
|
| 685 |
+
medsam_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/medsam_unfreezed/model.safetensors",
|
| 686 |
+
unet_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/unet5_attn/model.safetensors",
|
| 687 |
+
unet_teacher_kwargs: dict | None = None,
|
| 688 |
+
use_dwt: bool = True,
|
| 689 |
+
wavelet: str = "haar",
|
| 690 |
+
dwt_bands: list[str] | None = None,
|
| 691 |
+
use_shape: bool = False,
|
| 692 |
+
shape_res: int = 64,
|
| 693 |
+
**kwargs,
|
| 694 |
+
):
|
| 695 |
+
if kwargs:
|
| 696 |
+
unknown = ", ".join(sorted(kwargs.keys()))
|
| 697 |
+
raise TypeError(f"Unexpected UNet2DAttn kwargs: {unknown}")
|
| 698 |
+
|
| 699 |
+
self.attn_start = max(0, int(attn_start))
|
| 700 |
+
self.use_attn = bool(use_attn)
|
| 701 |
+
self.use_shape = bool(use_shape)
|
| 702 |
+
self.img_size = int(img_size)
|
| 703 |
+
self.shape_res = int(shape_res)
|
| 704 |
+
self.criterion = DiceBCELoss()
|
| 705 |
+
self.steps_counter = 0
|
| 706 |
+
|
| 707 |
+
# Compute max channels needed
|
| 708 |
+
max_channels = self.size * (2 ** (self.depth + 1))
|
| 709 |
+
|
| 710 |
+
# Create shared attention modulator
|
| 711 |
+
if self.use_attn:
|
| 712 |
+
# Count total number of layers that will use attention
|
| 713 |
+
n_attn_layers = 0
|
| 714 |
+
for i in range(self.depth):
|
| 715 |
+
if i >= self.attn_start:
|
| 716 |
+
n_attn_layers += 2 # encoder has 2 conv blocks per level
|
| 717 |
+
n_attn_layers += 2 # bottleneck
|
| 718 |
+
for i in range(self.depth):
|
| 719 |
+
if i >= self.attn_start:
|
| 720 |
+
n_attn_layers += 2 # decoder has 2 conv blocks per level
|
| 721 |
+
|
| 722 |
+
self.shared_attn = SharedAttnModulator(
|
| 723 |
+
n_organs=self.n_organs,
|
| 724 |
+
n_layers=n_attn_layers,
|
| 725 |
+
max_channels=max_channels,
|
| 726 |
+
img_size=self.img_size,
|
| 727 |
+
patch_size=patch_size,
|
| 728 |
+
emb_dim=emb_dim,
|
| 729 |
+
n_heads=n_heads,
|
| 730 |
+
use_dwt=use_dwt,
|
| 731 |
+
wavelet=wavelet,
|
| 732 |
+
dwt_bands=dwt_bands,
|
| 733 |
+
use_shape=use_shape,
|
| 734 |
+
shape_res=shape_res,
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
# ---------------- Encoder ----------------
|
| 738 |
+
self.encoder = nn.ModuleDict()
|
| 739 |
+
|
| 740 |
+
# First encoder block
|
| 741 |
+
if self.use_attn and 0 >= self.attn_start:
|
| 742 |
+
self.encoder["0"] = DownConvBlockFiLM(
|
| 743 |
+
[self.in_channels, self.size],
|
| 744 |
+
[self.size, self.size * 2],
|
| 745 |
+
)
|
| 746 |
+
else:
|
| 747 |
+
self.encoder["0"] = DownConvBlock(
|
| 748 |
+
[self.in_channels, self.size], [self.size, self.size * 2]
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
# Remaining encoder blocks
|
| 752 |
+
for i in range(1, self.depth):
|
| 753 |
+
in_ch = [self.size * (2**i), self.size * (2**i)]
|
| 754 |
+
out_ch = [self.size * (2**i), self.size * (2 ** (i + 1))]
|
| 755 |
+
key = str(i)
|
| 756 |
+
|
| 757 |
+
if self.use_attn and i >= self.attn_start:
|
| 758 |
+
self.encoder[key] = DownConvBlockFiLM(in_ch, out_ch)
|
| 759 |
+
else:
|
| 760 |
+
self.encoder[key] = DownConvBlock(in_ch, out_ch)
|
| 761 |
+
|
| 762 |
+
# ---------------- Bottleneck ----------------
|
| 763 |
+
if self.use_attn:
|
| 764 |
+
self.bottleneck = UpConvBlockFiLM(
|
| 765 |
+
[self.size * (2**self.depth), self.size * (2**self.depth)],
|
| 766 |
+
[self.size * (2**self.depth), self.size * (2 ** (self.depth + 1))],
|
| 767 |
+
)
|
| 768 |
+
else:
|
| 769 |
+
self.bottleneck = UpConvBlock(
|
| 770 |
+
[self.size * (2**self.depth), self.size * (2**self.depth)],
|
| 771 |
+
[self.size * (2**self.depth), self.size * (2 ** (self.depth + 1))],
|
| 772 |
+
)
|
| 773 |
+
|
| 774 |
+
# ---------------- Decoder ----------------
|
| 775 |
+
self.decoder = nn.ModuleDict()
|
| 776 |
+
|
| 777 |
+
for i in range(self.depth, 1, -1):
|
| 778 |
+
use_attn_at_level = self.use_attn and (i - 1) >= self.attn_start
|
| 779 |
+
|
| 780 |
+
if use_attn_at_level:
|
| 781 |
+
self.decoder[str(i - 1)] = UpConvBlockFiLM(
|
| 782 |
+
[
|
| 783 |
+
self.size * (2 ** (i + 1)) + self.size * (2**i),
|
| 784 |
+
self.size * (2**i),
|
| 785 |
+
],
|
| 786 |
+
[self.size * (2**i), self.size * (2**i)],
|
| 787 |
+
)
|
| 788 |
+
else:
|
| 789 |
+
self.decoder[str(i - 1)] = UpConvBlock(
|
| 790 |
+
[
|
| 791 |
+
self.size * (2 ** (i + 1)) + self.size * (2**i),
|
| 792 |
+
self.size * (2**i),
|
| 793 |
+
],
|
| 794 |
+
[self.size * (2**i), self.size * (2**i)],
|
| 795 |
+
)
|
| 796 |
+
|
| 797 |
+
# Final decoder block
|
| 798 |
+
if self.use_attn and 0 >= self.attn_start:
|
| 799 |
+
self.decoder["0"] = UpConvBlockFiLM(
|
| 800 |
+
[self.size * 4 + self.size * 2, self.size * 2],
|
| 801 |
+
[self.size * 2, self.size * 2],
|
| 802 |
+
up_conv=False,
|
| 803 |
+
)
|
| 804 |
+
else:
|
| 805 |
+
self.decoder["0"] = UpConvBlock(
|
| 806 |
+
[self.size * 4 + self.size * 2, self.size * 2],
|
| 807 |
+
[self.size * 2, self.size * 2],
|
| 808 |
+
up_conv=False,
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
self.out_layer = ConvBlock(
|
| 812 |
+
self.size * 2,
|
| 813 |
+
self.out_channels,
|
| 814 |
+
conv_kwargs={"kernel_size": 1, "stride": 1, "padding": 0},
|
| 815 |
+
)
|
| 816 |
+
self._init_distillation(
|
| 817 |
+
distill=distill,
|
| 818 |
+
distill_unet=distill_unet,
|
| 819 |
+
medsam_teacher_ckpt=medsam_teacher_ckpt,
|
| 820 |
+
unet_teacher_ckpt=unet_teacher_ckpt,
|
| 821 |
+
unet_teacher_kwargs=unet_teacher_kwargs,
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
def _build_layer_configs(self):
|
| 825 |
+
"""
|
| 826 |
+
Build list of (layer_id, n_channels) for all layers that use attention.
|
| 827 |
+
Layer IDs are sequential: 0, 1, 2, ...
|
| 828 |
+
"""
|
| 829 |
+
configs = []
|
| 830 |
+
layer_id = 0
|
| 831 |
+
|
| 832 |
+
# Encoder layers
|
| 833 |
+
for i in range(self.depth):
|
| 834 |
+
if self.use_attn and i >= self.attn_start:
|
| 835 |
+
# Each encoder block has 2 conv outputs
|
| 836 |
+
configs.append((layer_id, self.size * (2**i)))
|
| 837 |
+
layer_id += 1
|
| 838 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 839 |
+
layer_id += 1
|
| 840 |
+
|
| 841 |
+
# Bottleneck (2 conv blocks)
|
| 842 |
+
if self.use_attn:
|
| 843 |
+
configs.append((layer_id, self.size * (2**self.depth)))
|
| 844 |
+
layer_id += 1
|
| 845 |
+
configs.append((layer_id, self.size * (2 ** (self.depth + 1))))
|
| 846 |
+
layer_id += 1
|
| 847 |
+
|
| 848 |
+
# Decoder layers
|
| 849 |
+
for i in range(self.depth - 1, -1, -1):
|
| 850 |
+
if self.use_attn and i >= self.attn_start:
|
| 851 |
+
# Each decoder block has 2 conv outputs
|
| 852 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 853 |
+
layer_id += 1
|
| 854 |
+
configs.append((layer_id, self.size * (2 ** (i + 1))))
|
| 855 |
+
layer_id += 1
|
| 856 |
+
|
| 857 |
+
return configs
|
| 858 |
+
|
| 859 |
+
def _prepare_forward(
|
| 860 |
+
self,
|
| 861 |
+
*,
|
| 862 |
+
pixel_values: torch.Tensor,
|
| 863 |
+
organ_id: torch.Tensor | None = None,
|
| 864 |
+
**kwargs,
|
| 865 |
+
) -> dict:
|
| 866 |
+
forward_ctx = {
|
| 867 |
+
"mod_list": None,
|
| 868 |
+
"mod_idx": 0,
|
| 869 |
+
"projected_shapes": None,
|
| 870 |
+
}
|
| 871 |
+
if self.use_attn:
|
| 872 |
+
layer_configs = self._build_layer_configs()
|
| 873 |
+
modulations, projected_shapes = self.shared_attn.compute_all_modulations(
|
| 874 |
+
pixel_values, organ_id, layer_configs
|
| 875 |
+
)
|
| 876 |
+
forward_ctx["mod_list"] = [modulations[i] for i in range(len(layer_configs))]
|
| 877 |
+
forward_ctx["projected_shapes"] = projected_shapes
|
| 878 |
+
return forward_ctx
|
| 879 |
+
|
| 880 |
+
def _next_modulation(self, forward_ctx: dict):
|
| 881 |
+
mod_idx = forward_ctx["mod_idx"]
|
| 882 |
+
mod_list = forward_ctx["mod_list"]
|
| 883 |
+
gammas = [mod_list[mod_idx][0], mod_list[mod_idx + 1][0]]
|
| 884 |
+
betas = [mod_list[mod_idx][1], mod_list[mod_idx + 1][1]]
|
| 885 |
+
forward_ctx["mod_idx"] = mod_idx + 2
|
| 886 |
+
return gammas, betas
|
| 887 |
+
|
| 888 |
+
def _encode(
|
| 889 |
+
self,
|
| 890 |
+
layer: nn.Module,
|
| 891 |
+
x: torch.Tensor,
|
| 892 |
+
organ_id: torch.Tensor | None = None,
|
| 893 |
+
forward_ctx: dict | None = None,
|
| 894 |
+
):
|
| 895 |
+
if isinstance(layer, DownConvBlockFiLM):
|
| 896 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 897 |
+
return layer(x, gammas, betas)
|
| 898 |
+
return layer(x)
|
| 899 |
+
|
| 900 |
+
def _bottleneck(
|
| 901 |
+
self,
|
| 902 |
+
x: torch.Tensor,
|
| 903 |
+
organ_id: torch.Tensor | None = None,
|
| 904 |
+
forward_ctx: dict | None = None,
|
| 905 |
+
):
|
| 906 |
+
if isinstance(self.bottleneck, UpConvBlockFiLM):
|
| 907 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 908 |
+
return self.bottleneck(x, gammas, betas)
|
| 909 |
+
return self.bottleneck(x)
|
| 910 |
+
|
| 911 |
+
def _decode(
|
| 912 |
+
self,
|
| 913 |
+
layer: nn.Module,
|
| 914 |
+
x: torch.Tensor,
|
| 915 |
+
organ_id: torch.Tensor | None = None,
|
| 916 |
+
forward_ctx: dict | None = None,
|
| 917 |
+
):
|
| 918 |
+
if isinstance(layer, UpConvBlockFiLM):
|
| 919 |
+
gammas, betas = self._next_modulation(forward_ctx)
|
| 920 |
+
return layer(x, gammas, betas)
|
| 921 |
+
return layer(x)
|
| 922 |
+
|
| 923 |
+
def _apply_auxiliary_losses(
|
| 924 |
+
self,
|
| 925 |
+
*,
|
| 926 |
+
loss: torch.Tensor | float,
|
| 927 |
+
logits: torch.Tensor,
|
| 928 |
+
masks: torch.Tensor | None,
|
| 929 |
+
organ_id: torch.Tensor | None = None,
|
| 930 |
+
forward_ctx: dict | None = None,
|
| 931 |
+
**kwargs,
|
| 932 |
+
):
|
| 933 |
+
self.steps_counter += 1
|
| 934 |
+
if not self.use_shape or masks is None:
|
| 935 |
+
return loss
|
| 936 |
+
|
| 937 |
+
projected_shapes = forward_ctx.get("projected_shapes", None)
|
| 938 |
+
if projected_shapes is None or projected_shapes.numel() == 0:
|
| 939 |
+
return loss
|
| 940 |
+
|
| 941 |
+
reduced_masks = canonicalize_mask_batch_normalized(
|
| 942 |
+
masks, canon_res=self.shape_res
|
| 943 |
+
)
|
| 944 |
+
loss_shape = torch.tensor([0.0], requires_grad=True).to(logits.device)
|
| 945 |
+
for projected_shape in projected_shapes:
|
| 946 |
+
loss_shape = loss_shape + self.criterion(projected_shape, reduced_masks)
|
| 947 |
+
|
| 948 |
+
loss_shape = loss_shape / projected_shapes.shape[0]
|
| 949 |
+
if wandb.run is not None:
|
| 950 |
+
wandb.log({"shape_loss": loss_shape.item()}, commit=False)
|
| 951 |
+
loss = loss + loss_shape
|
| 952 |
+
|
| 953 |
+
if self.steps_counter % 200 == 0:
|
| 954 |
+
with torch.no_grad():
|
| 955 |
+
proj_tokens = torch.sigmoid(
|
| 956 |
+
self.shared_attn.shape_proj(
|
| 957 |
+
self.shared_attn.shape_embed.weight[:8].clone().detach()
|
| 958 |
+
)
|
| 959 |
+
.reshape(8, self.shape_res, self.shape_res)
|
| 960 |
+
.detach()
|
| 961 |
+
)
|
| 962 |
+
proj_tokens = (proj_tokens > 0.5).to(torch.float32)
|
| 963 |
+
proj_vis = (
|
| 964 |
+
proj_tokens.view(2, 4, self.shape_res, self.shape_res)
|
| 965 |
+
.permute(0, 2, 1, 3)
|
| 966 |
+
.reshape(self.shape_res * 2, self.shape_res * 4)
|
| 967 |
+
)
|
| 968 |
+
wandb.log({"projected_tokens": wandb.Image(proj_vis.unsqueeze(0))})
|
| 969 |
+
print("log")
|
| 970 |
+
|
| 971 |
+
return loss
|
unet_base.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from copy import deepcopy
|
| 2 |
+
from abc import ABC, abstractmethod
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from safetensors.torch import load_file
|
| 6 |
+
from torch import nn
|
| 7 |
+
from torchvision.transforms import v2
|
| 8 |
+
import wandb
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class BaseUnet(nn.Module, ABC):
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
in_channels: int,
|
| 15 |
+
num_classes: int,
|
| 16 |
+
n_organs: int,
|
| 17 |
+
size: int = 32,
|
| 18 |
+
depth: int = 3,
|
| 19 |
+
**kwargs
|
| 20 |
+
):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.in_channels = in_channels
|
| 23 |
+
self.out_channels = num_classes
|
| 24 |
+
self.n_organs = n_organs
|
| 25 |
+
self.size = size
|
| 26 |
+
self.depth = depth
|
| 27 |
+
self.distill = False
|
| 28 |
+
self.distill_unet = False
|
| 29 |
+
self.distill_model = None
|
| 30 |
+
self.distill_adapter = None
|
| 31 |
+
self.distill_loss = None
|
| 32 |
+
self._build_model(**kwargs)
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def _pad_to_2d(x: torch.Tensor, stride: int):
|
| 36 |
+
h, w = x.shape[-2:]
|
| 37 |
+
|
| 38 |
+
new_h = h if h % stride == 0 else h + stride - (h % stride)
|
| 39 |
+
new_w = w if w % stride == 0 else w + stride - (w % stride)
|
| 40 |
+
|
| 41 |
+
top = (new_h - h) // 2
|
| 42 |
+
bottom = (new_h - h) - top
|
| 43 |
+
left = (new_w - w) // 2
|
| 44 |
+
right = (new_w - w) - left
|
| 45 |
+
|
| 46 |
+
pads = (left, right, top, bottom)
|
| 47 |
+
x_pad = F.pad(x, pads, mode="constant", value=0)
|
| 48 |
+
return x_pad, pads
|
| 49 |
+
|
| 50 |
+
@staticmethod
|
| 51 |
+
def _unpad_2d(x: torch.Tensor, pads):
|
| 52 |
+
left, right, top, bottom = pads
|
| 53 |
+
|
| 54 |
+
if top or bottom:
|
| 55 |
+
end_h = -bottom if bottom > 0 else None
|
| 56 |
+
x = x[:, :, top:end_h, :]
|
| 57 |
+
|
| 58 |
+
if left or right:
|
| 59 |
+
end_w = -right if right > 0 else None
|
| 60 |
+
x = x[:, :, :, left:end_w]
|
| 61 |
+
|
| 62 |
+
return x
|
| 63 |
+
|
| 64 |
+
@abstractmethod
|
| 65 |
+
def _build_model(self, **kwargs):
|
| 66 |
+
pass
|
| 67 |
+
|
| 68 |
+
def _prepare_forward(
|
| 69 |
+
self,
|
| 70 |
+
*,
|
| 71 |
+
pixel_values: torch.Tensor,
|
| 72 |
+
organ_id: torch.Tensor | None = None,
|
| 73 |
+
**kwargs,
|
| 74 |
+
) -> dict:
|
| 75 |
+
return {}
|
| 76 |
+
|
| 77 |
+
@abstractmethod
|
| 78 |
+
def _encode(
|
| 79 |
+
self,
|
| 80 |
+
layer: nn.Module,
|
| 81 |
+
x: torch.Tensor,
|
| 82 |
+
organ_id: torch.Tensor | None = None,
|
| 83 |
+
forward_ctx: dict | None = None,
|
| 84 |
+
):
|
| 85 |
+
pass
|
| 86 |
+
|
| 87 |
+
@abstractmethod
|
| 88 |
+
def _bottleneck(
|
| 89 |
+
self,
|
| 90 |
+
x: torch.Tensor,
|
| 91 |
+
organ_id: torch.Tensor | None = None,
|
| 92 |
+
forward_ctx: dict | None = None,
|
| 93 |
+
):
|
| 94 |
+
pass
|
| 95 |
+
|
| 96 |
+
@abstractmethod
|
| 97 |
+
def _decode(
|
| 98 |
+
self,
|
| 99 |
+
layer: nn.Module,
|
| 100 |
+
x: torch.Tensor,
|
| 101 |
+
organ_id: torch.Tensor | None = None,
|
| 102 |
+
forward_ctx: dict | None = None,
|
| 103 |
+
):
|
| 104 |
+
pass
|
| 105 |
+
|
| 106 |
+
def encode(
|
| 107 |
+
self,
|
| 108 |
+
x: torch.Tensor,
|
| 109 |
+
organ_id: torch.Tensor | None = None,
|
| 110 |
+
forward_ctx: dict | None = None,
|
| 111 |
+
):
|
| 112 |
+
feat_list = []
|
| 113 |
+
pads = None
|
| 114 |
+
|
| 115 |
+
pre_padding = (
|
| 116 |
+
(x.size(-1) % 2**self.depth != 0)
|
| 117 |
+
or (x.size(-2) % 2**self.depth != 0)
|
| 118 |
+
or (x.size(-3) % 2**self.depth != 0)
|
| 119 |
+
)
|
| 120 |
+
if pre_padding:
|
| 121 |
+
x, pads = self._pad_to_2d(x, 2**self.depth)
|
| 122 |
+
|
| 123 |
+
out, feat = self._encode(
|
| 124 |
+
self.encoder["0"], x, organ_id=organ_id, forward_ctx=forward_ctx
|
| 125 |
+
)
|
| 126 |
+
feat_list.append(feat)
|
| 127 |
+
|
| 128 |
+
for key in list(self.encoder.keys())[1:]:
|
| 129 |
+
out, feat = self._encode(
|
| 130 |
+
self.encoder[key], out, organ_id=organ_id, forward_ctx=forward_ctx
|
| 131 |
+
)
|
| 132 |
+
feat_list.append(feat)
|
| 133 |
+
|
| 134 |
+
out = self._bottleneck(out, organ_id=organ_id, forward_ctx=forward_ctx)
|
| 135 |
+
return out, feat_list, pads
|
| 136 |
+
|
| 137 |
+
def decode(
|
| 138 |
+
self,
|
| 139 |
+
out: torch.Tensor,
|
| 140 |
+
feat_list: list[torch.Tensor],
|
| 141 |
+
pads,
|
| 142 |
+
organ_id: torch.Tensor | None = None,
|
| 143 |
+
forward_ctx: dict | None = None,
|
| 144 |
+
):
|
| 145 |
+
for key in self.decoder:
|
| 146 |
+
out = self._decode(
|
| 147 |
+
self.decoder[key],
|
| 148 |
+
torch.cat((out, feat_list[int(key)]), dim=1),
|
| 149 |
+
organ_id=organ_id,
|
| 150 |
+
forward_ctx=forward_ctx,
|
| 151 |
+
)
|
| 152 |
+
del feat_list[int(key)]
|
| 153 |
+
|
| 154 |
+
out = self.out_layer(out)
|
| 155 |
+
if pads is not None:
|
| 156 |
+
out = self._unpad_2d(out, pads).squeeze(1)
|
| 157 |
+
return out
|
| 158 |
+
|
| 159 |
+
def _apply_auxiliary_losses(
|
| 160 |
+
self,
|
| 161 |
+
*,
|
| 162 |
+
loss: torch.Tensor | float,
|
| 163 |
+
logits: torch.Tensor,
|
| 164 |
+
masks: torch.Tensor | None,
|
| 165 |
+
organ_id: torch.Tensor | None = None,
|
| 166 |
+
forward_ctx: dict | None = None,
|
| 167 |
+
**kwargs,
|
| 168 |
+
):
|
| 169 |
+
return loss
|
| 170 |
+
|
| 171 |
+
def _init_distillation(
|
| 172 |
+
self,
|
| 173 |
+
*,
|
| 174 |
+
distill: bool = False,
|
| 175 |
+
distill_unet: bool = False,
|
| 176 |
+
medsam_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/medsam_unfreezed/model.safetensors",
|
| 177 |
+
unet_teacher_ckpt: str = "/work/phd_ultrasounds/UUSIC_new/checkpoints/unet5_attn_distilled/model.safetensors",
|
| 178 |
+
unet_teacher_kwargs: dict | None = None,
|
| 179 |
+
):
|
| 180 |
+
if distill and distill_unet:
|
| 181 |
+
raise ValueError("distill and distill_unet cannot both be enabled.")
|
| 182 |
+
|
| 183 |
+
self.distill = bool(distill)
|
| 184 |
+
self.distill_unet = bool(distill_unet)
|
| 185 |
+
self.distill_model = None
|
| 186 |
+
self.distill_adapter = None
|
| 187 |
+
self.distill_loss = None
|
| 188 |
+
|
| 189 |
+
if not self.distill and not self.distill_unet:
|
| 190 |
+
return
|
| 191 |
+
|
| 192 |
+
from .segm_net import DistillationLoss, MedSAM
|
| 193 |
+
|
| 194 |
+
student_channels = (2048 // (32 // self.size)) // (2 ** (5 - self.depth))
|
| 195 |
+
|
| 196 |
+
if self.distill:
|
| 197 |
+
self.distill_adapter = nn.Conv2d(student_channels, 256, kernel_size=1)
|
| 198 |
+
else:
|
| 199 |
+
self.distill_adapter = nn.Conv2d(student_channels, 2048, kernel_size=1)
|
| 200 |
+
|
| 201 |
+
if self.distill:
|
| 202 |
+
from segment_anything import sam_model_registry
|
| 203 |
+
from utils.paths import MEDSAM_BASE_WEIGHTS
|
| 204 |
+
|
| 205 |
+
sam_model = sam_model_registry["vit_b"](checkpoint=MEDSAM_BASE_WEIGHTS)
|
| 206 |
+
self.distill_model = MedSAM(
|
| 207 |
+
image_encoder=deepcopy(sam_model.image_encoder),
|
| 208 |
+
mask_decoder=deepcopy(sam_model.mask_decoder),
|
| 209 |
+
prompt_encoder=deepcopy(sam_model.prompt_encoder),
|
| 210 |
+
predict_bboxes=True,
|
| 211 |
+
freeze_image_encoder=0,
|
| 212 |
+
)
|
| 213 |
+
state_dict = load_file(medsam_teacher_ckpt)
|
| 214 |
+
load_result = self.distill_model.load_state_dict(state_dict)
|
| 215 |
+
print(f"Loaded MedSam teacher model and loaded weights:\n{load_result}")
|
| 216 |
+
else:
|
| 217 |
+
from .unet_attn import UNet2DAttn
|
| 218 |
+
|
| 219 |
+
teacher_kwargs = {
|
| 220 |
+
"in_channels": 3,
|
| 221 |
+
"num_classes": 1,
|
| 222 |
+
"n_organs": 10,
|
| 223 |
+
"size": 32,
|
| 224 |
+
"depth": 5,
|
| 225 |
+
"attn_start": 0,
|
| 226 |
+
"use_attn": True,
|
| 227 |
+
"img_size": 512,
|
| 228 |
+
"patch_size": 8,
|
| 229 |
+
"emb_dim": 768,
|
| 230 |
+
"n_heads": 8,
|
| 231 |
+
"distill": False,
|
| 232 |
+
"distill_unet": False,
|
| 233 |
+
"use_dwt": False,
|
| 234 |
+
"wavelet": "haar",
|
| 235 |
+
"use_shape": False,
|
| 236 |
+
"shape_res": 64,
|
| 237 |
+
}
|
| 238 |
+
if unet_teacher_kwargs is not None:
|
| 239 |
+
teacher_kwargs.update(unet_teacher_kwargs)
|
| 240 |
+
|
| 241 |
+
self.distill_model = UNet2DAttn(**teacher_kwargs)
|
| 242 |
+
state_dict = load_file(unet_teacher_ckpt)
|
| 243 |
+
state_dict = {k: v for k, v in state_dict.items() if "distill" not in k}
|
| 244 |
+
load_result = self.distill_model.load_state_dict(state_dict)
|
| 245 |
+
print(f"Loaded UNet teacher model and loaded weights:\n{load_result}")
|
| 246 |
+
|
| 247 |
+
for p in self.distill_model.parameters():
|
| 248 |
+
p.requires_grad = False
|
| 249 |
+
self.distill_model.eval()
|
| 250 |
+
self.distill_loss = DistillationLoss()
|
| 251 |
+
|
| 252 |
+
def _forward_distillation(
|
| 253 |
+
self,
|
| 254 |
+
*,
|
| 255 |
+
student_logits: torch.Tensor,
|
| 256 |
+
student_bottleneck: torch.Tensor,
|
| 257 |
+
pixel_values: torch.Tensor,
|
| 258 |
+
organ_id: torch.Tensor | None = None,
|
| 259 |
+
pixel_values_medsam: torch.Tensor | None = None,
|
| 260 |
+
) -> torch.Tensor | None:
|
| 261 |
+
if not self.distill and not self.distill_unet:
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
if self.distill:
|
| 265 |
+
if pixel_values_medsam is None:
|
| 266 |
+
raise ValueError("pixel_values_medsam is required when distill=True.")
|
| 267 |
+
|
| 268 |
+
with torch.no_grad():
|
| 269 |
+
up_pixel_values = v2.functional.resize(
|
| 270 |
+
pixel_values_medsam, 1024, v2.InterpolationMode.BICUBIC
|
| 271 |
+
)
|
| 272 |
+
image_embedding = self.distill_model.image_encoder(up_pixel_values)
|
| 273 |
+
|
| 274 |
+
student_resized = F.interpolate(
|
| 275 |
+
student_bottleneck,
|
| 276 |
+
size=(image_embedding.shape[-1], image_embedding.shape[-1]),
|
| 277 |
+
mode="bilinear",
|
| 278 |
+
align_corners=False,
|
| 279 |
+
)
|
| 280 |
+
up_feat = self.distill_adapter(student_resized)
|
| 281 |
+
distill_loss_emb = self.distill_loss(
|
| 282 |
+
student_logits=up_feat,
|
| 283 |
+
teacher_logits=image_embedding.detach(),
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
if wandb.run is not None:
|
| 287 |
+
wandb.log(
|
| 288 |
+
{
|
| 289 |
+
"distill_loss_emb": distill_loss_emb["loss"].item(),
|
| 290 |
+
},
|
| 291 |
+
commit=False,
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
return distill_loss_emb["loss"]
|
| 295 |
+
|
| 296 |
+
with torch.no_grad():
|
| 297 |
+
forward_ctx = self._prepare_forward(
|
| 298 |
+
pixel_values=pixel_values,
|
| 299 |
+
organ_id=organ_id,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
teacher_embedding, _, _ = self.encode(
|
| 303 |
+
pixel_values, organ_id=organ_id, forward_ctx=forward_ctx
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
student_resized = F.interpolate(
|
| 307 |
+
student_bottleneck,
|
| 308 |
+
size=(teacher_embedding.shape[-1], teacher_embedding.shape[-1]),
|
| 309 |
+
mode="bilinear",
|
| 310 |
+
align_corners=False,
|
| 311 |
+
)
|
| 312 |
+
up_feat = self.distill_adapter(student_resized)
|
| 313 |
+
distill_loss_emb = self.distill_loss(
|
| 314 |
+
student_logits=student_bottleneck,
|
| 315 |
+
teacher_logits=teacher_embedding.detach(),
|
| 316 |
+
)
|
| 317 |
+
if wandb.run is not None:
|
| 318 |
+
wandb.log(
|
| 319 |
+
{"distill_loss_logits": distill_loss_emb["loss"].item()},
|
| 320 |
+
commit=False,
|
| 321 |
+
)
|
| 322 |
+
return distill_loss_emb["loss"]
|
| 323 |
+
|
| 324 |
+
def forward(
|
| 325 |
+
self,
|
| 326 |
+
pixel_values,
|
| 327 |
+
organ_id=None,
|
| 328 |
+
labels=None,
|
| 329 |
+
masks=None,
|
| 330 |
+
bbox_coords=None,
|
| 331 |
+
organ_id_metric=None,
|
| 332 |
+
teacher_embedding=None,
|
| 333 |
+
teacher_mask=None,
|
| 334 |
+
pixel_values_medsam=None,
|
| 335 |
+
**kwargs,
|
| 336 |
+
):
|
| 337 |
+
forward_ctx = self._prepare_forward(
|
| 338 |
+
pixel_values=pixel_values,
|
| 339 |
+
organ_id=organ_id,
|
| 340 |
+
masks=masks,
|
| 341 |
+
**kwargs,
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
out_bottleneck, feat_list, pads = self.encode(
|
| 345 |
+
pixel_values, organ_id=organ_id, forward_ctx=forward_ctx
|
| 346 |
+
)
|
| 347 |
+
out = self.decode(
|
| 348 |
+
out_bottleneck,
|
| 349 |
+
feat_list,
|
| 350 |
+
pads,
|
| 351 |
+
organ_id=organ_id,
|
| 352 |
+
forward_ctx=forward_ctx,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
if masks is not None:
|
| 356 |
+
loss = self.criterion(out, masks)
|
| 357 |
+
else:
|
| 358 |
+
loss = 0.0
|
| 359 |
+
|
| 360 |
+
distill_loss = self._forward_distillation(
|
| 361 |
+
student_logits=out,
|
| 362 |
+
student_bottleneck=out_bottleneck,
|
| 363 |
+
pixel_values=pixel_values,
|
| 364 |
+
organ_id=organ_id,
|
| 365 |
+
pixel_values_medsam=pixel_values_medsam,
|
| 366 |
+
)
|
| 367 |
+
if distill_loss is not None:
|
| 368 |
+
loss = loss + distill_loss
|
| 369 |
+
|
| 370 |
+
loss = self._apply_auxiliary_losses(
|
| 371 |
+
loss=loss,
|
| 372 |
+
logits=out,
|
| 373 |
+
masks=masks,
|
| 374 |
+
organ_id=organ_id,
|
| 375 |
+
forward_ctx=forward_ctx,
|
| 376 |
+
pixel_values=pixel_values,
|
| 377 |
+
**kwargs,
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
return {
|
| 381 |
+
"loss": loss,
|
| 382 |
+
"logits": out,
|
| 383 |
+
"labels": masks,
|
| 384 |
+
"organ_id": organ_id,
|
| 385 |
+
"organ_id_metric": organ_id_metric,
|
| 386 |
+
}
|