Morelli001 commited on
Commit
8966f37
·
verified ·
1 Parent(s): 0259e49

Upload folder using huggingface_hub

Browse files
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
+ }