AI-RESEARCHER-2024 commited on
Commit
e4a7008
·
verified ·
1 Parent(s): d487db0

Upload 314 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. README.md +71 -56
  3. app.py +122 -351
  4. medclipseg.py +172 -0
  5. prepare_medclip_data.py +115 -0
  6. processing.py +841 -92
  7. reference_medclipseg/LICENSE +201 -0
  8. reference_medclipseg/README.md +141 -0
  9. reference_medclipseg/biomedclip/biomedclip.py +80 -0
  10. reference_medclipseg/biomedclip/layers/__init__.py +62 -0
  11. reference_medclipseg/biomedclip/layers/activations.py +173 -0
  12. reference_medclipseg/biomedclip/layers/activations_me.py +208 -0
  13. reference_medclipseg/biomedclip/layers/adaptive_avgmax_pool.py +183 -0
  14. reference_medclipseg/biomedclip/layers/attention2d.py +351 -0
  15. reference_medclipseg/biomedclip/layers/attention_pool.py +105 -0
  16. reference_medclipseg/biomedclip/layers/attention_pool2d.py +278 -0
  17. reference_medclipseg/biomedclip/layers/blur_pool.py +91 -0
  18. reference_medclipseg/biomedclip/layers/bottleneck_attn.py +157 -0
  19. reference_medclipseg/biomedclip/layers/cbam.py +112 -0
  20. reference_medclipseg/biomedclip/layers/classifier.py +283 -0
  21. reference_medclipseg/biomedclip/layers/cond_conv2d.py +123 -0
  22. reference_medclipseg/biomedclip/layers/config.py +165 -0
  23. reference_medclipseg/biomedclip/layers/conv2d_same.py +110 -0
  24. reference_medclipseg/biomedclip/layers/conv_bn_act.py +92 -0
  25. reference_medclipseg/biomedclip/layers/create_act.py +138 -0
  26. reference_medclipseg/biomedclip/layers/create_attn.py +89 -0
  27. reference_medclipseg/biomedclip/layers/create_conv2d.py +36 -0
  28. reference_medclipseg/biomedclip/layers/create_norm.py +60 -0
  29. reference_medclipseg/biomedclip/layers/create_norm_act.py +95 -0
  30. reference_medclipseg/biomedclip/layers/drop.py +182 -0
  31. reference_medclipseg/biomedclip/layers/eca.py +145 -0
  32. reference_medclipseg/biomedclip/layers/evo_norm.py +352 -0
  33. reference_medclipseg/biomedclip/layers/fast_norm.py +206 -0
  34. reference_medclipseg/biomedclip/layers/filter_response_norm.py +68 -0
  35. reference_medclipseg/biomedclip/layers/format.py +58 -0
  36. reference_medclipseg/biomedclip/layers/gather_excite.py +90 -0
  37. reference_medclipseg/biomedclip/layers/global_context.py +67 -0
  38. reference_medclipseg/biomedclip/layers/grid.py +49 -0
  39. reference_medclipseg/biomedclip/layers/grn.py +39 -0
  40. reference_medclipseg/biomedclip/layers/halo_attn.py +233 -0
  41. reference_medclipseg/biomedclip/layers/helpers.py +43 -0
  42. reference_medclipseg/biomedclip/layers/hybrid_embed.py +253 -0
  43. reference_medclipseg/biomedclip/layers/inplace_abn.py +87 -0
  44. reference_medclipseg/biomedclip/layers/interpolate.py +68 -0
  45. reference_medclipseg/biomedclip/layers/lambda_layer.py +134 -0
  46. reference_medclipseg/biomedclip/layers/layer_scale.py +38 -0
  47. reference_medclipseg/biomedclip/layers/linear.py +19 -0
  48. reference_medclipseg/biomedclip/layers/median_pool.py +49 -0
  49. reference_medclipseg/biomedclip/layers/mixed_conv2d.py +51 -0
  50. reference_medclipseg/biomedclip/layers/ml_decoder.py +146 -0
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ reference_medclipseg/data/Covid19/Prompts_Folder/Train_text.xlsx filter=lfs diff=lfs merge=lfs -text
37
+ reference_medclipseg/data/EUS/Prompts_Folder/Test_text_original.xlsx filter=lfs diff=lfs merge=lfs -text
38
+ reference_medclipseg/data/EUS/Prompts_Folder/Train_text_25.xlsx filter=lfs diff=lfs merge=lfs -text
39
+ reference_medclipseg/data/EUS/Prompts_Folder/Train_text_50.xlsx filter=lfs diff=lfs merge=lfs -text
40
+ reference_medclipseg/data/EUS/Prompts_Folder/Train_text.xlsx filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,8 +1,6 @@
1
  ---
2
- title: Cochlear Neurofilament Tracer
3
  emoji: 🧠
4
- colorFrom: indigo
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
@@ -10,67 +8,84 @@ pinned: false
10
  license: mit
11
  ---
12
 
13
- # 🧠 Cochlear Neurofilament Tracer
14
 
15
- A HuggingFace app that traces auditory-nerve fibers in confocal z-stacks of the
16
- organ of Corti and quantifies them **per frequency region**, separating
17
- **IHC-innervating** from **OHC-innervating** fibers.
18
 
19
- It is an alternative to IMARIS filament tracing that keeps each neuron as a
20
- **single continuous traced element** instead of splitting it into many
21
- threshold-dependent segments.
 
 
 
22
 
23
- ## Input
 
 
 
 
 
24
 
25
- - **File type:** Zeiss `.czi` 3D z-stacks. Generic `.tif/.tiff` stacks are also
26
- accepted for flexibility.
27
- - **Channels:**
28
- - *Neurofilament* traces the neuron.
29
- - *Myo7a* — marks hair cells; used as a reference to separate IHC- vs
30
- OHC-innervating fibers. IHCs form a single row and OHCs form three adjacent
31
- rows, so the Myo7a band is used to place the IHC/OHC boundary.
32
- - **Frequency region:** selectable (8/16/22/32/64 kHz), auto-detected from the
33
- file name when possible.
34
- - Channels are auto-detected from CZI metadata (Alexa-555 → Neurofilament,
35
- Alexa-405 → Myo7a) but can be reassigned in the UI.
36
 
37
- ## What it does
38
-
39
- 1. Segments and **skeletonises the Neurofilament network in 3D** using physical
40
- voxel spacing (from CZI metadata, or entered for TIFF).
41
- 2. Uses the **Myo7a channel** to place an IHC/OHC boundary. This can be set
42
- manually (ROI 1 vs ROI 2) by moving the boundary slider while viewing the
43
- Myo7a preview, choosing the split axis, and choosing which side is IHC.
44
- 3. Computes, per region (Whole field / IHC / OHC):
45
- - **Number of fibers** (continuous skeleton components above a minimum length)
46
- - **Thickness / diameter** (from the 3D distance transform)
47
- - **Length** (µm, spacing-aware)
48
- - **Branching** (number of branch points)
49
- - **Area covered** within the field of view (µm² and % of FOV)
50
-
51
- ## Output
52
 
53
- - A **black-background image** of the traced neurons in **white** (skeletonised
54
- trace), plus a colour-coded IHC/OHC overlay.
55
- - An **Excel workbook** with all quantification, organized by frequency region,
56
- with IHC and OHC fibers reported separately (tidy "Per region" sheet plus
57
- per-metric frequency × region summary sheets).
 
58
 
59
- The **Batch** tab processes several stacks at once (e.g. all frequency regions
60
- of one cochlea) and compiles one Excel workbook plus a ZIP of skeleton images.
 
 
 
 
 
 
61
 
62
- ## Notes on method
 
 
63
 
64
- Confocal images of the organ of Corti are dense, so fully separating every
65
- individual axon is inherently ambiguous. This tool traces the network
66
- continuously and reports metrics **per region surrounding the IHCs / OHCs**,
67
- with a human-in-the-loop boundary for reliable IHC vs OHC assignment. The
68
- `sensitivity` control scales the segmentation threshold to capture more or fewer
69
- thin fibers.
 
 
 
 
 
 
 
 
 
70
 
71
- ## Local run
 
 
 
 
 
 
 
 
72
 
73
- ```bash
74
- pip install -r requirements.txt
75
- python app.py
76
- ```
 
 
 
 
 
1
  ---
2
+ title: Neuron Quantification using AI — MedCLIPSeg
3
  emoji: 🧠
 
 
4
  sdk: gradio
5
  sdk_version: 5.49.1
6
  app_file: app.py
 
8
  license: mit
9
  ---
10
 
11
+ # Neuron Quantification using AI — MedCLIPSeg variant
12
 
13
+ Iman Sabir Ezzat, Randa K Ismail, Ayden Chavez, Marisa Zallocchi, PhD, Steven Fernandes, PhD
 
 
14
 
15
+ A variant of the neuron tracer that applies the **MedCLIPSeg** approach
16
+ (Koleilat et al., *Probabilistic Vision–Language Adaptation for Data-Efficient
17
+ and Generalizable Medical Image Segmentation*, CVPR 2026): a **text prompt**
18
+ describes the target ("nerve fibers …"), a frozen **CLIP** backbone produces a
19
+ **fiber-probability map**, and a probabilistic (Monte-Carlo) step yields a
20
+ pixel-level **uncertainty map**.
21
 
22
+ ## What the app does
23
+ - **Classical trace (reliable):** the deterministic 3D tracer produces the
24
+ quantification you should trust — number of fibers, total length, diameter,
25
+ branch points.
26
+ - **MedCLIP (text-prompted):** shows the CLIP fiber-probability and uncertainty
27
+ maps for the neurofilament channel. The prompt is editable.
28
 
29
+ ## Few-shot training (included)
30
+ A runnable, few-shot version of the MedCLIPSeg approach — **frozen CLIP encoders +
31
+ a text-conditioned decoder** — is trained here on **8 animals** (1A, 1B, 2A, 3A,
32
+ 3B, 4Aa, 4Bb, 5A) and **tested on the unseen C1 and C2b**:
 
 
 
 
 
 
 
33
 
34
+ ```bash
35
+ python train_medclip_fewshot.py # runs on CPU/MPS; saves weights_medclip_decoder.pt
36
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ **Result: held-out (C1 + C2b) mean Dice 0.53** (per image 0.36–0.60; train Dice
39
+ = 1.0, i.e. memorised). The segmentation lands on the fiber band and is far
40
+ better than zero-shot, but it is **coarse** CLIP features are 14×14, so thin
41
+ fibers are only roughly captured, and the label is IMARIS's approximate
42
+ reconstruction. The app loads `weights_medclip_decoder.pt` automatically and uses
43
+ the trained model.
44
 
45
+ ## Honest scope
46
+ The full authors' model (`reference_medclipseg/`, PVL adapters + probabilistic
47
+ attention) needs a **CUDA GPU**; the included few-shot decoder is the part that
48
+ runs here. Either way the MedCLIP output is a **segmentation prior** — the
49
+ **trustworthy quantification (length, fibers, diameter) comes from the classical
50
+ tracer**, and the *length number* does not generalize reliably across animals
51
+ (measured earlier). Without a trained decoder the app falls back to zero-shot
52
+ CLIP, which is only a coarse prior.
53
 
54
+ ## To get the real, trained MedCLIPSeg
55
+ The original authors' code is bundled in **`reference_medclipseg/`** (needs a
56
+ **CUDA GPU**). Two steps:
57
 
58
+ 1. **Build the dataset** (no manual masks needed uses IMARIS's reconstruction
59
+ as the label):
60
+ ```bash
61
+ python prepare_medclip_data.py
62
+ # -> data/Neurofilament/{Train,Val,Test}_Folder/{img,label} + Prompts_Folder/*.xlsx
63
+ ```
64
+ 2. **Fine-tune** the model on a GPU:
65
+ ```bash
66
+ cd reference_medclipseg
67
+ pip install -r requirements.txt
68
+ python train.py --config-file configs/Neurofilament.yaml
69
+ python test.py --config-file configs/Neurofilament.yaml # segmentation + uncertainty
70
+ ```
71
+ (`configs/Neurofilament.yaml` uses a BiomedCLIP backbone; switch `CLIP_MODEL`
72
+ to `clip`/`pubmedclip`/`unimedclip` as desired.)
73
 
74
+ ## Honest limitations (unchanged from the main project)
75
+ - The training **label** is IMARIS's *reconstructed* skeleton (from its segment
76
+ coordinates), which is approximate, and IMARIS itself **over-traces** the true
77
+ fibre length (~3–4×). So a trained model would imitate IMARIS, not ground truth.
78
+ - Predicting IMARIS's numbers does **not** generalize well across animals
79
+ (measured R² ≈ 0 in earlier experiments). MedCLIPSeg's segmentation may look
80
+ cleaner, but the **length number** remains unreliable on unseen animals.
81
+ - For exact IMARIS values, read the IMARIS statistics directly (as in the other
82
+ tools).
83
 
84
+ ## Files
85
+ - `app.py` the Gradio variant (classical trace + MedCLIP maps).
86
+ - `medclipseg.py` — runnable text-prompted CLIP segmentation + uncertainty.
87
+ - `prepare_medclip_data.py` — builds the MedCLIPSeg dataset from CZIs + IMARIS
88
+ reconstruction masks.
89
+ - `reference_medclipseg/` — the original MedCLIPSeg code (for GPU fine-tuning) +
90
+ `configs/Neurofilament.yaml`.
91
+ - `processing.py` — the shared image-processing / tracing pipeline.
app.py CHANGED
@@ -1,373 +1,144 @@
1
- """
2
- Cochlear Neurofilament Tracer — HuggingFace Gradio app
3
- ======================================================
4
 
5
- Traces auditory-nerve fibers (Neurofilament channel) in confocal z-stacks of
6
- the organ of Corti, uses the Myo7a hair-cell channel to separate
7
- IHC-innervating from OHC-innervating fibers, and reports per-region
8
- quantification (number of fibers, diameter, length, branch points, area
9
- covered) plus a black-background skeleton image and an Excel workbook.
10
 
11
- Accepts Zeiss **CZI** z-stacks and generic **TIFF** stacks.
 
 
 
12
  """
13
-
14
- import os
15
- import tempfile
16
- import zipfile
17
  import traceback
18
-
19
  import numpy as np
20
  import pandas as pd
21
  import gradio as gr
22
- from skimage.io import imsave
23
- from scipy import ndimage as ndi
24
 
25
  import processing as P
 
26
 
27
- OUT_DIR = tempfile.mkdtemp(prefix="neuron_tracer_")
28
-
29
- METRIC_COLS = [
30
- "File", "Frequency region", "Region", "Number of fibers",
31
- "Total length (um)", "Mean diameter (um)", "Median diameter (um)",
32
- "Branch points", "Area covered (um^2)", "FOV area (um^2)",
33
- "Area covered (% of FOV)",
34
- ]
35
-
36
-
37
- # --------------------------------------------------------------------------- #
38
- # Small helpers
39
- # --------------------------------------------------------------------------- #
40
-
41
- def _channel_choices(img: P.LoadedImage):
42
- choices = []
43
- for i, ch in enumerate(img.channels):
44
- dye = ch.get("dye") or "no dye / transmitted"
45
- choices.append((f"Ch {i} — {ch.get('name','?')} ({dye})", i))
46
- return choices
47
-
48
-
49
- def _draw_boundary_on(gray_u8, boundary_frac, axis="Y"):
50
- """Return an RGB copy of a grayscale MIP with a yellow boundary line."""
51
- rgb = np.stack([gray_u8] * 3, axis=-1)
52
- ny, nx = gray_u8.shape
53
- if axis.upper() == "Y":
54
- b = min(max(int(round(boundary_frac * ny)), 0), ny - 1)
55
- rgb[b, :] = (255, 255, 0)
56
- else:
57
- b = min(max(int(round(boundary_frac * nx)), 0), nx - 1)
58
- rgb[:, b] = (255, 255, 0)
59
- return rgb
60
-
61
-
62
- def _save_png(arr, name):
63
- path = os.path.join(OUT_DIR, name)
64
- imsave(path, arr)
65
- return path
66
-
67
 
68
- def _build_excel(rows, path):
69
- """Write a tidy per-region sheet plus a frequency×region summary sheet."""
70
- df = pd.DataFrame(rows, columns=METRIC_COLS)
71
- with pd.ExcelWriter(path, engine="openpyxl") as xl:
72
- df.to_excel(xl, sheet_name="Per region", index=False)
73
- # Summary: only IHC / OHC rows, pivoted by frequency region.
74
- sub = df[df["Region"].isin(["IHC region", "OHC region"])]
75
- if not sub.empty:
76
- for metric in ["Number of fibers", "Total length (um)",
77
- "Mean diameter (um)", "Branch points",
78
- "Area covered (um^2)"]:
79
- piv = sub.pivot_table(index="Frequency region",
80
- columns="Region", values=metric,
81
- aggfunc="mean")
82
- sheet = metric.split(" (")[0][:28]
83
- piv.to_excel(xl, sheet_name=f"{sheet}")
84
- return df
85
 
 
 
 
86
 
87
- # --------------------------------------------------------------------------- #
88
- # Single-image interactive flow
89
- # --------------------------------------------------------------------------- #
90
 
91
- def load_and_preview(file_obj, dz, dy, dx):
92
  if file_obj is None:
93
- return (None, gr.update(), gr.update(), gr.update(),
94
- gr.update(), None, None, "Please upload a CZI or TIFF file.")
95
  try:
96
- img = P.load_image(file_obj, dz=float(dz), dy=float(dy), dx=float(dx))
97
- except Exception as e:
98
- return (None, gr.update(), gr.update(), gr.update(),
99
- gr.update(), None, None,
100
- f"❌ Failed to load file:\n{e}\n{traceback.format_exc()}")
101
-
102
- nf, myo = P.guess_channels(img)
103
- choices = _channel_choices(img)
104
- freq = P.detect_frequency(img.source_name)
105
- bfrac = P.suggest_boundary(img.data[myo])
106
-
107
- nf_prev = P.channel_preview(img.data[nf])
108
- myo_prev = _draw_boundary_on(P.channel_preview(img.data[myo]), bfrac, "Y")
109
-
110
- dz_, dy_, dx_ = img.voxel
111
- status = (f"✅ Loaded **{img.source_name}** — shape "
112
- f"{img.data.shape} (C,Z,Y,X)\n\n"
113
- f"Voxel size: dz={dz_:.3f}, dy={dy_:.4f}, dx={dx_:.4f} µm. "
114
- f"Detected frequency region: **{freq}**.\n\n"
115
- f"Auto-picked Neurofilament = Ch {nf}, Myo7a = Ch {myo}. "
116
- f"Adjust below if needed, then press **Run analysis**.")
117
- return (img,
118
- gr.update(choices=choices, value=nf),
119
- gr.update(choices=choices, value=myo),
120
- gr.update(value=freq),
121
- gr.update(value=round(bfrac, 3)),
122
- nf_prev, myo_prev, status)
123
-
124
-
125
- def refresh_boundary_preview(img, myo_idx, boundary, axis):
126
- if img is None or myo_idx is None:
127
- return None
128
- myo_prev = P.channel_preview(img.data[int(myo_idx)])
129
- return _draw_boundary_on(myo_prev, float(boundary), axis)
130
-
131
-
132
- def run_single(img, nf_idx, myo_idx, freq, axis, ihc_side, boundary,
133
- sensitivity, min_fiber):
134
- if img is None:
135
- return None, None, None, None, None, "Please load an image first."
136
- if nf_idx is None:
137
- return None, None, None, None, None, "Please select the Neurofilament channel."
138
- try:
139
- nf_idx, myo_idx = int(nf_idx), int(myo_idx)
140
- trace = P.trace_neurites(img.data[nf_idx], img.voxel,
141
- sensitivity=float(sensitivity))
142
- shape_yx = img.data[nf_idx].shape[1:]
143
- side = "low" if ihc_side.startswith("Low") else "high"
144
- ihc_roi, ohc_roi = P.make_region_masks(shape_yx, float(boundary),
145
- ihc_side=side, axis=axis)
146
-
147
- whole = P.compute_metrics(trace, "Whole field",
148
- min_fiber_um=float(min_fiber))
149
- m_ihc = P.compute_metrics(trace, "IHC region", ihc_roi,
150
- min_fiber_um=float(min_fiber))
151
- m_ohc = P.compute_metrics(trace, "OHC region", ohc_roi,
152
- min_fiber_um=float(min_fiber))
153
-
154
- skel_img = P.skeleton_image(trace.skeleton)
155
- region_img = P.region_overlay(trace.skeleton, ihc_roi, ohc_roi,
156
- float(boundary), axis)
157
- myo_img = _draw_boundary_on(P.channel_preview(img.data[myo_idx]),
158
- float(boundary), axis)
159
-
160
- stem = os.path.splitext(img.source_name)[0]
161
- skel_path = _save_png(skel_img, f"{stem}_skeleton.png")
162
-
163
- rows = []
164
- for m in (whole, m_ihc, m_ohc):
165
- r = m.as_row()
166
- r = {"File": img.source_name, "Frequency region": freq, **r}
167
- rows.append(r)
168
- df = pd.DataFrame(rows, columns=METRIC_COLS)
169
- xl_path = os.path.join(OUT_DIR, f"{stem}_quantification.xlsx")
170
- _build_excel(rows, xl_path)
171
-
172
- status = (f"✅ Done. Traced {int(trace.skeleton.sum())} skeleton voxels. "
173
- f"IHC={m_ihc.n_fibers} fibers / {m_ihc.total_length_um:.0f} µm, "
174
- f"OHC={m_ohc.n_fibers} fibers / {m_ohc.total_length_um:.0f} µm.")
175
- # Return skeleton path also as downloadable file
176
- return (skel_img, region_img, myo_img, df,
177
- [skel_path, xl_path], status)
178
- except Exception as e:
179
- return (None, None, None, None, None,
180
- f"❌ Error:\n{e}\n{traceback.format_exc()}")
181
-
182
-
183
- # --------------------------------------------------------------------------- #
184
- # Batch flow
185
- # --------------------------------------------------------------------------- #
186
-
187
- def run_batch(files, axis, ihc_side, sensitivity, min_fiber, dz, dy, dx,
188
- progress=gr.Progress()):
189
- if not files:
190
- return None, None, None, "Please upload one or more files."
191
- side = "low" if ihc_side.startswith("Low") else "high"
192
- all_rows, gallery, skel_paths = [], [], []
193
- log = []
194
- for f in progress.tqdm(files, desc="Processing"):
195
- path = f if isinstance(f, str) else f.name
196
- name = os.path.basename(path)
197
- try:
198
- img = P.load_image(path, dz=float(dz), dy=float(dy), dx=float(dx))
199
- nf, myo = P.guess_channels(img)
200
- freq = P.detect_frequency(name)
201
- trace = P.trace_neurites(img.data[nf], img.voxel,
202
- sensitivity=float(sensitivity))
203
- bfrac = P.suggest_boundary(img.data[myo])
204
- shape_yx = img.data[nf].shape[1:]
205
- ihc_roi, ohc_roi = P.make_region_masks(shape_yx, bfrac,
206
- ihc_side=side, axis=axis)
207
- for m in (P.compute_metrics(trace, "Whole field",
208
- min_fiber_um=float(min_fiber)),
209
- P.compute_metrics(trace, "IHC region", ihc_roi,
210
- min_fiber_um=float(min_fiber)),
211
- P.compute_metrics(trace, "OHC region", ohc_roi,
212
- min_fiber_um=float(min_fiber))):
213
- all_rows.append({"File": name, "Frequency region": freq,
214
- **m.as_row()})
215
- skel_img = P.skeleton_image(trace.skeleton)
216
- stem = os.path.splitext(name)[0]
217
- sp = _save_png(skel_img, f"{stem}_skeleton.png")
218
- skel_paths.append(sp)
219
- gallery.append((skel_img, f"{name} ({freq})"))
220
- log.append(f"✅ {name}: {freq}")
221
- except Exception as e:
222
- log.append(f"❌ {name}: {e}")
223
-
224
- if not all_rows:
225
- return None, None, gallery, "No files processed.\n" + "\n".join(log)
226
-
227
- xl_path = os.path.join(OUT_DIR, "batch_quantification.xlsx")
228
- df = _build_excel(all_rows, xl_path)
229
-
230
- zip_path = os.path.join(OUT_DIR, "batch_skeletons.zip")
231
- with zipfile.ZipFile(zip_path, "w") as z:
232
- for sp in skel_paths:
233
- z.write(sp, os.path.basename(sp))
234
- z.write(xl_path, os.path.basename(xl_path))
235
-
236
- return df, [xl_path, zip_path], gallery, "\n".join(log)
237
-
238
-
239
- # --------------------------------------------------------------------------- #
240
- # UI
241
- # --------------------------------------------------------------------------- #
242
-
243
- INTRO = """
244
- # 🧠 Cochlear Neurofilament Tracer
245
-
246
- Trace auditory-nerve fibers in confocal z-stacks and quantify them **per
247
- frequency region**, separating **IHC-innervating** from **OHC-innervating**
248
- fibers using the Myo7a hair-cell channel.
249
-
250
- **Channels expected:** *Neurofilament* (traces the neuron) and *Myo7a* (hair
251
- cells — reference to split IHC vs OHC). IHCs form a single row, OHCs form three
252
- rows, so the Myo7a band is used to place the IHC/OHC boundary — which you can
253
- fine-tune by hand.
254
-
255
- **Input:** Zeiss `.czi` z-stacks or generic `.tif/.tiff` stacks.
256
  """
257
 
258
- with gr.Blocks(title="Cochlear Neurofilament Tracer", theme=gr.themes.Soft()) as demo:
259
- gr.Markdown(INTRO)
260
-
261
- with gr.Tab("Single image (interactive)"):
262
- img_state = gr.State()
263
- with gr.Row():
264
- with gr.Column(scale=1):
265
- file_in = gr.File(label="Upload CZI or TIFF",
266
- file_types=[".czi", ".tif", ".tiff"],
267
- type="filepath")
268
- with gr.Accordion("Voxel size (µm) — used for TIFF; CZI reads "
269
- "its own", open=False):
270
- dz_in = gr.Number(0.35, label="dz (µm/plane)")
271
- dy_in = gr.Number(0.0895, label="dy (µm/px)")
272
- dx_in = gr.Number(0.0895, label="dx (µm/px)")
273
- load_btn = gr.Button("① Load & preview", variant="secondary")
274
-
275
- nf_dd = gr.Dropdown(label="Neurofilament channel", choices=[])
276
- myo_dd = gr.Dropdown(label="Myo7a channel", choices=[])
277
- freq_dd = gr.Dropdown(label="Frequency region",
278
- choices=P.FREQ_CHOICES,
279
- value="Other / unknown")
280
-
281
- gr.Markdown("**IHC / OHC region split** (Myo7a-guided)")
282
- axis_dd = gr.Radio(["Y", "X"], value="Y",
283
- label="Split axis (Y = radial, usual)")
284
- side_dd = gr.Radio(["Low side = IHC", "High side = IHC"],
285
- value="Low side = IHC",
286
- label="Which side is IHC?")
287
- boundary_sl = gr.Slider(0.0, 1.0, value=0.5, step=0.005,
288
- label="Boundary position (fraction "
289
- "along split axis)")
290
-
291
- gr.Markdown("**Tracing**")
292
- sens_sl = gr.Slider(0.5, 1.5, value=1.0, step=0.05,
293
- label="Sensitivity (↑ = capture more/thinner "
294
- "fibers)")
295
- minfib_sl = gr.Slider(0.0, 20.0, value=5.0, step=0.5,
296
- label="Min fiber length to count (µm)")
297
- run_btn = gr.Button("② Run analysis", variant="primary")
298
-
299
- with gr.Column(scale=2):
300
- status = gr.Markdown()
301
- with gr.Row():
302
- nf_prev = gr.Image(label="Neurofilament (MIP)",
303
- height=220)
304
- myo_prev = gr.Image(label="Myo7a (MIP) + boundary",
305
- height=220)
306
- skel_out = gr.Image(label="Traced neurons — white on black",
307
- height=300)
308
- region_out = gr.Image(label="Region overlay (cyan = IHC, "
309
- "magenta = OHC)", height=300)
310
- table = gr.Dataframe(label="Quantification", wrap=True)
311
- files_out = gr.Files(label="Downloads (skeleton PNG + Excel)")
312
-
313
- load_btn.click(load_and_preview,
314
- [file_in, dz_in, dy_in, dx_in],
315
- [img_state, nf_dd, myo_dd, freq_dd, boundary_sl,
316
- nf_prev, myo_prev, status])
317
- # Live boundary preview
318
- for comp in (boundary_sl, myo_dd, axis_dd):
319
- comp.change(refresh_boundary_preview,
320
- [img_state, myo_dd, boundary_sl, axis_dd], myo_prev)
321
- run_btn.click(run_single,
322
- [img_state, nf_dd, myo_dd, freq_dd, axis_dd, side_dd,
323
- boundary_sl, sens_sl, minfib_sl],
324
- [skel_out, region_out, myo_prev, table, files_out, status])
325
-
326
- with gr.Tab("Batch (multiple images)"):
327
- gr.Markdown(
328
- "Upload several z-stacks (e.g. all frequency regions of one "
329
- "cochlea). Each is auto-traced with an auto-placed IHC/OHC "
330
- "boundary, and results are combined into one Excel workbook "
331
- "organized by frequency region.")
332
- with gr.Row():
333
- with gr.Column(scale=1):
334
- batch_files = gr.File(label="Upload CZI/TIFF files",
335
- file_count="multiple",
336
- file_types=[".czi", ".tif", ".tiff"],
337
- type="filepath")
338
- b_axis = gr.Radio(["Y", "X"], value="Y", label="Split axis")
339
- b_side = gr.Radio(["Low side = IHC", "High side = IHC"],
340
- value="Low side = IHC",
341
- label="Which side is IHC?")
342
- b_sens = gr.Slider(0.5, 1.5, value=1.0, step=0.05,
343
- label="Sensitivity")
344
- b_minfib = gr.Slider(0.0, 20.0, value=5.0, step=0.5,
345
- label="Min fiber length (µm)")
346
- with gr.Accordion("Voxel size (µm) for TIFF", open=False):
347
- b_dz = gr.Number(0.35, label="dz")
348
- b_dy = gr.Number(0.0895, label="dy")
349
- b_dx = gr.Number(0.0895, label="dx")
350
- batch_btn = gr.Button("Run batch", variant="primary")
351
- with gr.Column(scale=2):
352
- batch_log = gr.Textbox(label="Log", lines=6)
353
- batch_table = gr.Dataframe(label="Combined quantification",
354
- wrap=True)
355
- batch_files_out = gr.Files(label="Downloads (Excel + ZIP)")
356
- batch_gallery = gr.Gallery(label="Skeleton traces",
357
- columns=3, height=400)
358
- batch_btn.click(run_batch,
359
- [batch_files, b_axis, b_side, b_sens, b_minfib,
360
- b_dz, b_dy, b_dx],
361
- [batch_table, batch_files_out, batch_gallery, batch_log])
362
 
363
- gr.Markdown(
364
- "---\n*Method:* the Neurofilament channel is smoothed, thresholded "
365
- "(Otsu, scaled by the sensitivity control) and skeletonised in 3D; "
366
- "length, diameter (from the 3D distance transform), branch points and "
367
- "footprint area are measured with physical voxel spacing. Each fiber is "
368
- "a connected skeleton component ≥ the minimum length. The Myo7a band "
369
- "defines the IHC/OHC boundary, and metrics are reported for each "
370
- "region.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
372
  if __name__ == "__main__":
373
  demo.launch()
 
1
+ """Neuron Quantification using AI — MedCLIPSeg variant.
 
 
2
 
3
+ Same task as the main tracer, applying the MedCLIPSeg (CVPR 2026) vision-language
4
+ approach: a text prompt describes the target ("nerve fibers ..."), a frozen CLIP
5
+ backbone produces a fiber-probability map and a pixel-level UNCERTAINTY map.
 
 
6
 
7
+ The reliable quantification (length, fibers, diameter, branch points) still comes
8
+ from the deterministic tracer. The MedCLIP maps are shown alongside; zero-shot
9
+ general CLIP is only a coarse prior on this domain — train the bundled model
10
+ (reference_medclipseg/) on a GPU with masks for real segmentation quality.
11
  """
 
 
 
 
12
  import traceback
 
13
  import numpy as np
14
  import pandas as pd
15
  import gradio as gr
 
 
16
 
17
  import processing as P
18
+ import medclipseg as MC
19
 
20
+ METRIC_COLS = ["Region", "Number of fibers", "Total length (um)",
21
+ "Mean diameter (um)", "Branch points", "Area covered (um^2)"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ def _cm(arr, name):
25
+ import matplotlib.cm as cm
26
+ return (getattr(cm, name)(np.clip(arr, 0, 1))[..., :3] * 255).astype(np.uint8)
27
 
 
 
 
28
 
29
+ def analyze(file_obj, fg_prompt, run_medclip):
30
  if file_obj is None:
31
+ return None, None, None, None, None, "Upload a CZI or TIFF z-stack."
 
32
  try:
33
+ img = P.load_image(file_obj)
34
+ nf, _ = P.guess_channels(img)
35
+ nf_mip = P.channel_preview(img.data[nf])
36
+
37
+ # --- deterministic trace + quantification (the reliable numbers) ---
38
+ trace = P.trace_neurites(img.data[nf], img.voxel, sensitivity=1.0,
39
+ prune_um=3.0)
40
+ m = P.compute_metrics(trace, "Whole field", min_fiber_um=5.0)
41
+ overlay = P.overlay_on_original(nf_mip, trace.skeleton)
42
+ df = pd.DataFrame([{
43
+ "Region": "Whole field", "Number of fibers": m.n_fibers,
44
+ "Total length (um)": round(m.total_length_um, 2),
45
+ "Mean diameter (um)": round(m.mean_diameter_um, 3),
46
+ "Branch points": m.n_branch_points,
47
+ "Area covered (um^2)": round(m.area_covered_um2, 2)}],
48
+ columns=METRIC_COLS)
49
+ status = (f"**Classical trace:** {m.n_fibers} fibers / "
50
+ f"{m.total_length_um:.0f} µm (the reliable quantification).")
51
+
52
+ prob_img = unc_img = None
53
+ if run_medclip:
54
+ if fg_prompt and fg_prompt.strip():
55
+ MC.FG_PROMPTS = [p.strip() for p in fg_prompt.split("|") if p.strip()]
56
+ prob, unc = MC.segment_best(nf_mip)
57
+ prob_img, unc_img = _cm(prob, "viridis"), _cm(unc, "magma")
58
+ if MC.has_trained_model():
59
+ status += (f"\n\n**MedCLIP (few-shot trained, text-prompted):** "
60
+ f"fiber-probability + uncertainty maps (frozen CLIP + "
61
+ f"trained decoder; held-out Dice ≈ 0.53). Prompt(s): "
62
+ f"*{', '.join(MC.FG_PROMPTS)}*.")
63
+ else:
64
+ status += (f"\n\n**MedCLIP (zero-shot, text-prompted):** coarse "
65
+ f"prior train with train_medclip_fewshot.py for the "
66
+ f"few-shot model.")
67
+ return overlay, nf_mip, prob_img, unc_img, df, status
68
+ except Exception as e: # noqa: BLE001
69
+ return None, None, None, None, None, f"Error:\n{e}\n{traceback.format_exc()}"
70
+
71
+
72
+ HEADER_HTML = """
73
+ <div style="text-align:center; margin: 0.2rem 0 0.7rem;">
74
+ <h1 style="font-size:2.1rem; font-weight:750; letter-spacing:-0.01em;
75
+ margin:0 0 0.4rem;">Neuron Quantification using AI</h1>
76
+ <div style="font-weight:700; font-size:1.15rem; color:#1a1a1a;">
77
+ Iman Sabir Ezzat, Randa K Ismail, Ayden Chavez, Marisa Zallocchi, PhD, Steven Fernandes, PhD
78
+ </div>
79
+ <div style="font-weight:600; font-size:0.95rem; color:#4b5563; margin-top:0.25rem;">
80
+ MedCLIPSeg variant text-prompted vision-language segmentation
81
+ </div>
82
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  """
84
 
85
+ THEME = gr.themes.Base(
86
+ primary_hue=gr.themes.colors.slate, secondary_hue=gr.themes.colors.slate,
87
+ neutral_hue=gr.themes.colors.gray,
88
+ font=["system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"],
89
+ ).set(
90
+ body_background_fill="#ffffff", body_text_color="#1a1a1a",
91
+ background_fill_primary="#ffffff", background_fill_secondary="#f7f7f8",
92
+ block_background_fill="#ffffff", block_border_color="#e5e7eb",
93
+ block_label_text_color="#1a1a1a", block_title_text_color="#1a1a1a",
94
+ border_color_primary="#e5e7eb", button_primary_background_fill="#1f2937",
95
+ button_primary_text_color="#ffffff", input_background_fill="#ffffff",
96
+ input_border_color="#c0c5cc",
97
+ body_background_fill_dark="#ffffff", body_text_color_dark="#1a1a1a",
98
+ background_fill_primary_dark="#ffffff", background_fill_secondary_dark="#f7f7f8",
99
+ block_background_fill_dark="#ffffff", block_border_color_dark="#e5e7eb",
100
+ block_label_text_color_dark="#1a1a1a", block_title_text_color_dark="#1a1a1a",
101
+ panel_background_fill_dark="#ffffff", border_color_primary_dark="#e5e7eb",
102
+ button_primary_background_fill_dark="#1f2937",
103
+ button_primary_text_color_dark="#ffffff", input_background_fill_dark="#ffffff",
104
+ )
105
+ CSS = """
106
+ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
107
+ :root, .dark {
108
+ color-scheme: light; --body-background-fill:#ffffff;
109
+ --background-fill-primary:#ffffff; --block-background-fill:#ffffff;
110
+ --body-text-color:#1a1a1a; --block-label-text-color:#1a1a1a;
111
+ --block-title-text-color:#1a1a1a; --border-color-primary:#e5e7eb;
112
+ --input-background-fill:#ffffff; --neutral-950:#1a1a1a;
113
+ }
114
+ body, gradio-app, .gradio-container, .dark { background:#ffffff !important; color:#1a1a1a !important; }
115
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ with gr.Blocks(title="Neuron Quantification using AI — MedCLIPSeg",
118
+ theme=THEME, css=CSS) as demo:
119
+ gr.HTML(HEADER_HTML)
120
+ with gr.Row():
121
+ with gr.Column(scale=1):
122
+ file_in = gr.File(label="Neurofilament z-stack (.czi / .tif)",
123
+ type="filepath")
124
+ prompt = gr.Textbox(
125
+ label="Text prompt(s) for the target (separate with | )",
126
+ value="a fluorescence microscopy image of nerve fibers | "
127
+ "neurofilament nerve fibers and axons")
128
+ use_mc = gr.Checkbox(value=True,
129
+ label="Run MedCLIP text-prompted segmentation")
130
+ btn = gr.Button("Analyze", variant="primary")
131
+ with gr.Column(scale=2):
132
+ status = gr.Markdown()
133
+ with gr.Row():
134
+ out_overlay = gr.Image(label="Traced neurons (classical)", height=250)
135
+ out_orig = gr.Image(label="Neurofilament (MIP)", height=250)
136
+ with gr.Row():
137
+ out_prob = gr.Image(label="MedCLIP fiber probability", height=250)
138
+ out_unc = gr.Image(label="MedCLIP uncertainty", height=250)
139
+ out_table = gr.Dataframe(label="Quantification", wrap=True)
140
+ btn.click(analyze, [file_in, prompt, use_mc],
141
+ [out_overlay, out_orig, out_prob, out_unc, out_table, status])
142
 
143
  if __name__ == "__main__":
144
  demo.launch()
medclipseg.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MedCLIPSeg-style text-prompted segmentation, adapted to run here.
2
+
3
+ Faithful to the *approach* of Koleilat et al., "MedCLIPSeg: Probabilistic
4
+ Vision-Language Adaptation ..." (CVPR 2026): a FROZEN vision-language (CLIP)
5
+ backbone provides patch-level embeddings; short TEXT PROMPTS describe the target
6
+ ("nerve fibers ...") and the background; patch-text similarity yields a dense
7
+ segmentation probability, and a PROBABILISTIC step (Monte-Carlo sampling of the
8
+ patch embedding with an estimated variance — the paper samples attention Values
9
+ from learned distributions) yields a pixel-level UNCERTAINTY map.
10
+
11
+ This module is ZERO-SHOT (no training) using a general CLIP backbone, so on
12
+ cochlear neurofilament it is a *coarse prior*, not a trained segmentor. For the
13
+ real, trained model (the paper's PVL adapters + decoder, fine-tuned on masks)
14
+ see reference_medclipseg/ and train it on a GPU with the data from
15
+ prepare_medclip_data.py.
16
+ """
17
+ import os
18
+ import numpy as np
19
+ import torch
20
+ import torch.nn as nn
21
+ from skimage.transform import resize
22
+
23
+ _MODEL = "openai/clip-vit-base-patch16" # 16-px patches -> 14x14 per 224 tile
24
+ _clip = None
25
+ _proc = None
26
+
27
+ FG_PROMPTS = [
28
+ "a fluorescence microscopy image of nerve fibers",
29
+ "neurofilament nerve fibers and axons",
30
+ "a dense network of thin bright nerve fibers",
31
+ ]
32
+ BG_PROMPTS = [
33
+ "a black empty background",
34
+ "dark region with no tissue",
35
+ "background noise",
36
+ ]
37
+
38
+
39
+ def _load():
40
+ global _clip, _proc
41
+ if _clip is None:
42
+ from transformers import CLIPModel, CLIPProcessor
43
+ _clip = CLIPModel.from_pretrained(_MODEL).eval()
44
+ _proc = CLIPProcessor.from_pretrained(_MODEL)
45
+ return _clip, _proc
46
+
47
+
48
+ def _text_embeds(prompts):
49
+ clip, proc = _load()
50
+ tok = proc(text=prompts, return_tensors="pt", padding=True)
51
+ with torch.no_grad():
52
+ out = clip.text_model(input_ids=tok["input_ids"],
53
+ attention_mask=tok.get("attention_mask"))
54
+ t = clip.text_projection(out.pooler_output)
55
+ return t / t.norm(dim=-1, keepdim=True) # (K, D)
56
+
57
+
58
+ def _dense_patch_embeds(win_u8):
59
+ """Per-patch CLIP embeddings for a 224x224 uint8 RGB window (MaskCLIP-style)."""
60
+ clip, proc = _load()
61
+ px = proc(images=win_u8, return_tensors="pt")["pixel_values"]
62
+ vm = clip.vision_model
63
+ with torch.no_grad():
64
+ toks = vm(pixel_values=px).last_hidden_state[:, 1:, :] # drop CLS -> (1,196,768)
65
+ emb = clip.visual_projection(vm.post_layernorm(toks)) # (1,196,D)
66
+ emb = emb / emb.norm(dim=-1, keepdim=True)
67
+ return emb[0] # (196, D)
68
+
69
+
70
+ def segment(mip_u8, n_mc=8, sigma=0.05, tile=224):
71
+ """Text-prompted CLIP segmentation of a grayscale MIP.
72
+
73
+ Returns (prob, uncertainty) float maps at the input resolution, both in
74
+ [0, 1]. ``prob`` is the fiber probability; ``uncertainty`` is the std of the
75
+ probability across ``n_mc`` Monte-Carlo embedding samples.
76
+ """
77
+ H0, W0 = mip_u8.shape
78
+ rows = max(1, round(H0 / tile))
79
+ cols = max(1, round(W0 / tile))
80
+ Hr, Wr = rows * tile, cols * tile
81
+ rgb = np.stack([resize(mip_u8, (Hr, Wr), preserve_range=True)] * 3, -1).astype(np.uint8)
82
+
83
+ fg_t, bg_t = _text_embeds(FG_PROMPTS), _text_embeds(BG_PROMPTS)
84
+ gh, gw = 14, 14 # patches per tile (224/16)
85
+ prob = np.zeros((rows * gh, cols * gw), np.float32)
86
+ unc = np.zeros((rows * gh, cols * gw), np.float32)
87
+ for r in range(rows):
88
+ for c in range(cols):
89
+ win = rgb[r * tile:(r + 1) * tile, c * tile:(c + 1) * tile]
90
+ emb = _dense_patch_embeds(win) # (196, D)
91
+ samples = []
92
+ for m in range(n_mc):
93
+ e = emb if m == 0 else torch.nn.functional.normalize(
94
+ emb + sigma * torch.randn_like(emb), dim=-1)
95
+ fg = (e @ fg_t.T).mean(-1) # (196,)
96
+ bg = (e @ bg_t.T).mean(-1)
97
+ samples.append(torch.sigmoid((fg - bg) / 0.1).numpy())
98
+ samples = np.stack(samples).reshape(n_mc, gh, gw)
99
+ prob[r * gh:(r + 1) * gh, c * gw:(c + 1) * gw] = samples.mean(0)
100
+ unc[r * gh:(r + 1) * gh, c * gw:(c + 1) * gw] = samples.std(0)
101
+ prob = resize(prob, (H0, W0), order=1, preserve_range=True)
102
+ unc = resize(unc, (H0, W0), order=1, preserve_range=True)
103
+ # normalise uncertainty to [0,1] for display
104
+ unc = (unc - unc.min()) / (np.ptp(unc) + 1e-6)
105
+ return prob.astype(np.float32), unc.astype(np.float32)
106
+
107
+
108
+ # --------------------------------------------------------------------------- #
109
+ # Few-shot TRAINED decoder (frozen CLIP + text-conditioned decoder).
110
+ # Trained with train_medclip_fewshot.py (8 animals train, C1/C2b test).
111
+ # --------------------------------------------------------------------------- #
112
+
113
+ _WEIGHTS = os.path.join(os.path.dirname(os.path.abspath(__file__)),
114
+ "weights_medclip_decoder.pt")
115
+ _decoder = None
116
+
117
+
118
+ class _Decoder(nn.Module):
119
+ def __init__(self, cin=768, tdim=512, C=128):
120
+ super().__init__()
121
+ self.reduce = nn.Conv2d(cin, C, 1)
122
+ self.film = nn.Linear(tdim, 2 * C)
123
+
124
+ def up(ci, co):
125
+ return nn.Sequential(nn.ConvTranspose2d(ci, co, 2, 2),
126
+ nn.Conv2d(co, co, 3, padding=1),
127
+ nn.BatchNorm2d(co), nn.ReLU(True))
128
+ self.up = nn.Sequential(up(C, C), up(C, C), up(C, C // 2), up(C // 2, C // 4))
129
+ self.out = nn.Conv2d(C // 4, 1, 1)
130
+
131
+ def forward(self, feat, tvec):
132
+ x = self.reduce(feat)
133
+ g, b = self.film(tvec).chunk(2, -1)
134
+ x = x * (1 + g[:, :, None, None]) + b[:, :, None, None]
135
+ return torch.sigmoid(self.out(self.up(x)))
136
+
137
+
138
+ def _clip_patch_grid(mip_u8, size=224):
139
+ clip, proc = _load()
140
+ rgb = np.stack([resize(mip_u8, (size, size), preserve_range=True)] * 3, -1).astype(np.uint8)
141
+ px = proc(images=rgb, return_tensors="pt")["pixel_values"]
142
+ with torch.no_grad():
143
+ toks = clip.vision_model(pixel_values=px).last_hidden_state[0, 1:, :]
144
+ g = int(round(toks.shape[0] ** 0.5))
145
+ return toks.reshape(g, g, -1).permute(2, 0, 1).contiguous()[None] # (1,768,g,g)
146
+
147
+
148
+ def has_trained_model():
149
+ return os.path.exists(_WEIGHTS)
150
+
151
+
152
+ def segment_trained(mip_u8):
153
+ """Segment with the few-shot TRAINED decoder. Returns (prob, uncertainty)."""
154
+ global _decoder
155
+ if _decoder is None:
156
+ _decoder = _Decoder().eval()
157
+ _decoder.load_state_dict(torch.load(_WEIGHTS, map_location="cpu")["model"])
158
+ tvec = _text_embeds(FG_PROMPTS).mean(0, keepdim=True)
159
+ feat = _clip_patch_grid(mip_u8)
160
+ with torch.no_grad():
161
+ prob = _decoder(feat, tvec)[0, 0].numpy()
162
+ H0, W0 = mip_u8.shape
163
+ prob = resize(prob, (H0, W0), order=1, preserve_range=True).astype(np.float32)
164
+ unc = np.clip(4 * prob * (1 - prob), 0, 1).astype(np.float32) # entropy-like
165
+ return prob, unc
166
+
167
+
168
+ def segment_best(mip_u8):
169
+ """Use the trained few-shot decoder if available, else zero-shot CLIP."""
170
+ if has_trained_model():
171
+ return segment_trained(mip_u8)
172
+ return segment(mip_u8)
prepare_medclip_data.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build a MedCLIPSeg dataset for the neurofilament task (no manual masks needed).
2
+
3
+ For every CZI with an IMARIS _Statistics folder, writes:
4
+ data/Neurofilament/<split>_Folder/img/<name>.png (neurofilament MIP, RGB)
5
+ data/Neurofilament/<split>_Folder/label/<name>.png (IMARIS-reconstruction mask)
6
+ data/Neurofilament/Prompts_Folder/<split>_text.xlsx (Image, Ground Truth, Description)
7
+
8
+ The mask is the k-NN reconstruction of IMARIS's traced skeleton from its segment
9
+ coordinates (dilated) — the same approximate label used elsewhere. Splits are by
10
+ ANIMAL so validation/test are unseen animals. Then fine-tune the bundled model:
11
+
12
+ cd reference_medclipseg
13
+ python train.py --config-file configs/Neurofilament.yaml # needs a CUDA GPU
14
+
15
+ Honest note: this label is approximate (reconstructed, not IMARIS's exact voxel
16
+ mask), and the target is IMARIS's over-traced tangle — see README.
17
+ """
18
+ import argparse, glob, os, sys
19
+ import numpy as np
20
+ import pandas as pd
21
+ from scipy import ndimage as ndi
22
+ from scipy.spatial import cKDTree
23
+ from skimage.transform import resize
24
+ from skimage.io import imsave
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
+ import processing as P
28
+
29
+ ROOT = "/Users/slf20757/Desktop/Dr. Fernandes"
30
+ SIZE = 224
31
+ PROMPT = "a fluorescence microscopy image of neurofilament nerve fibers"
32
+ # animal -> split
33
+ VAL = {"C1"}
34
+ TEST = {"C2b"}
35
+
36
+
37
+ def seg_xyz(sd, stem):
38
+ def cols(name, nc):
39
+ r = []
40
+ for line in open(os.path.join(sd, f"{stem}_{name}.csv"), errors="ignore"):
41
+ p = line.split(",")
42
+ try:
43
+ r.append([float(p[i]) for i in range(nc)])
44
+ except (ValueError, IndexError):
45
+ pass
46
+ return np.array(r)
47
+ return cols("Segment_Position", 3)
48
+
49
+
50
+ def skel_mask(pos, voxel, shape_yx, k=3):
51
+ dz, dy, dx = voxel; ny, nx = shape_yx
52
+ col = pos[:, 0] / dx * (SIZE / nx); row = pos[:, 1] / dy * (SIZE / ny)
53
+ pts = np.stack([row, col], 1)
54
+ tree = cKDTree(pts); dist, idx = tree.query(pts, k=min(k + 1, len(pts)))
55
+ radius = 2.0 / dx * (SIZE / nx)
56
+ m = np.zeros((SIZE, SIZE), bool)
57
+ for i in range(len(pts)):
58
+ for j, d in zip(idx[i, 1:], dist[i, 1:]):
59
+ if d <= radius:
60
+ p0, p1 = pts[i], pts[j]
61
+ n = max(int(np.abs(p1 - p0).max()) + 1, 2)
62
+ t = np.linspace(0, 1, n)[:, None]; line = p0 + t * (p1 - p0)
63
+ m[np.clip(line[:, 0].astype(int), 0, SIZE - 1),
64
+ np.clip(line[:, 1].astype(int), 0, SIZE - 1)] = True
65
+ return ndi.binary_dilation(m, iterations=1)
66
+
67
+
68
+ def main():
69
+ ap = argparse.ArgumentParser()
70
+ ap.add_argument("--out", default="data/Neurofilament")
71
+ ap.add_argument("--limit", type=int, default=0, help="cap images (for testing)")
72
+ args = ap.parse_args()
73
+ rows = {"Train": [], "Val": [], "Test": []}
74
+ n = 0
75
+ for csv in sorted(glob.glob(os.path.join(ROOT, "*", "*_Statistics",
76
+ "*_Filament_Length_(sum).csv"))):
77
+ sd = os.path.dirname(csv)
78
+ stem = os.path.basename(csv).replace("_Filament_Length_(sum).csv", "")
79
+ animal = os.path.relpath(csv, ROOT).split(os.sep)[0]
80
+ czi = os.path.join(ROOT, animal, stem.replace(" filament", "") + ".czi")
81
+ if not (os.path.exists(czi) and
82
+ os.path.exists(os.path.join(sd, f"{stem}_Segment_Position_X.csv"))):
83
+ continue
84
+ split = "Val" if animal in VAL else "Test" if animal in TEST else "Train"
85
+ img = P.load_image(czi); nf, _ = P.guess_channels(img)
86
+ vol = img.data[nf]; ny, nx = vol.shape[1], vol.shape[2]
87
+ mip = resize(P.channel_preview(vol), (SIZE, SIZE), preserve_range=True).astype(np.uint8)
88
+ mask = (skel_mask(seg_xyz(sd, stem), img.voxel, (ny, nx)) * 255).astype(np.uint8)
89
+ name = stem.replace(" ", "_") + ".png"
90
+ for sub in ("img", "label"):
91
+ os.makedirs(os.path.join(args.out, f"{split}_Folder", sub), exist_ok=True)
92
+ imsave(os.path.join(args.out, f"{split}_Folder", "img", name),
93
+ np.stack([mip] * 3, -1))
94
+ imsave(os.path.join(args.out, f"{split}_Folder", "label", name), mask)
95
+ rows[split].append({"Image": name, "Ground Truth": name, "Description": PROMPT})
96
+ n += 1
97
+ print(f"[{split}] {stem}: mask px={int((mask>0).sum())}", flush=True)
98
+ del img, vol
99
+ if args.limit and n >= args.limit:
100
+ break
101
+
102
+ os.makedirs(os.path.join(args.out, "Prompts_Folder"), exist_ok=True)
103
+ for split, r in rows.items():
104
+ if r:
105
+ pd.DataFrame(r).to_excel(
106
+ os.path.join(args.out, "Prompts_Folder", f"{split}_text.xlsx"),
107
+ index=False)
108
+ print(f"\nDone. Train={len(rows['Train'])} Val={len(rows['Val'])} "
109
+ f"Test={len(rows['Test'])} -> {args.out}")
110
+ print("Now fine-tune (GPU): cd reference_medclipseg && "
111
+ "python train.py --config-file configs/Neurofilament.yaml")
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()
processing.py CHANGED
@@ -25,6 +25,18 @@ from skimage.filters import gaussian, threshold_otsu
25
  from skimage.morphology import remove_small_objects, skeletonize
26
  from skan import Skeleton, summarize
27
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # --------------------------------------------------------------------------- #
29
  # Data containers
30
  # --------------------------------------------------------------------------- #
@@ -59,19 +71,38 @@ class RegionMetrics:
59
  area_covered_um2: float = 0.0
60
  fov_area_um2: float = 0.0
61
  pct_area_covered: float = 0.0
 
 
 
 
 
 
62
 
63
  def as_row(self) -> dict:
64
- return {
 
 
 
 
65
  "Region": self.region,
66
  "Number of fibers": self.n_fibers,
 
 
 
67
  "Total length (um)": round(self.total_length_um, 2),
 
68
  "Mean diameter (um)": round(self.mean_diameter_um, 3),
69
  "Median diameter (um)": round(self.median_diameter_um, 3),
70
  "Branch points": self.n_branch_points,
 
 
 
 
71
  "Area covered (um^2)": round(self.area_covered_um2, 2),
72
  "FOV area (um^2)": round(self.fov_area_um2, 2),
73
  "Area covered (% of FOV)": round(self.pct_area_covered, 2),
74
  }
 
75
 
76
 
77
  @dataclass
@@ -165,30 +196,77 @@ def load_czi(path: str) -> LoadedImage:
165
  return LoadedImage(arr, channels, _czi_voxel(czi), os.path.basename(path))
166
 
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  def load_tiff(path: str, dz=0.35, dy=0.0895, dx=0.0895) -> LoadedImage:
169
  import tifffile
170
 
171
- arr = tifffile.imread(path)
172
- arr = np.squeeze(arr)
173
- # Heuristic to reach (C, Z, Y, X). The two largest axes are Y, X.
174
- if arr.ndim == 2: # (Y, X) single channel, single plane
175
- arr = arr[None, None]
176
- elif arr.ndim == 3:
177
- # Could be (Z,Y,X) single channel or (C,Y,X). Assume small first axis = C
178
- if arr.shape[0] <= 5:
179
- arr = arr[:, None] # (C, 1, Y, X)
180
- else:
181
- arr = arr[None] # (1, Z, Y, X)
182
- elif arr.ndim == 4:
183
- # find the two largest axes -> Y, X; of the remaining two the smaller = C
184
- yx = sorted(range(4), key=lambda a: arr.shape[a])[-2:]
185
- rest = [a for a in range(4) if a not in yx]
186
- c_axis = min(rest, key=lambda a: arr.shape[a])
187
- z_axis = [a for a in rest if a != c_axis][0]
188
- arr = np.transpose(arr, (c_axis, z_axis, *sorted(yx)))
189
  else:
190
- raise ValueError(f"Unsupported TIFF with {arr.ndim} dimensions")
191
- arr = arr.astype(np.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  channels = [{"name": f"Channel {i}", "dye": None, "color": None}
193
  for i in range(arr.shape[0])]
194
  return LoadedImage(arr, channels, (dz, dy, dx), os.path.basename(path))
@@ -203,31 +281,164 @@ def load_image(path: str, dz=0.35, dy=0.0895, dx=0.0895) -> LoadedImage:
203
  raise ValueError(f"Unsupported file type: {ext}")
204
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  # --------------------------------------------------------------------------- #
207
  # Channel identification
208
  # --------------------------------------------------------------------------- #
209
 
210
- # Wavelength-based hints: neurofilament here is Alexa-555 (red/green range),
211
- # Myo7a is Alexa-405 (blue). Transmitted-light PMT channels have no dye.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  def guess_channels(img: LoadedImage) -> tuple:
213
  """Best-effort (neurofilament_index, myo7a_index) from metadata + content."""
214
  nf_idx, myo_idx = None, None
215
- blue_like, red_like, plain = [], [], []
216
- for i, ch in enumerate(img.channels):
217
- dye = (ch.get("dye") or "").lower()
218
- color = (ch.get("color") or "").upper()
219
- ex = ch.get("ex")
220
- ex = float(ex) if ex else None
221
- if "405" in dye or (ex and ex < 430) or color == "#0000FF":
222
- blue_like.append(i)
223
- elif dye and dye not in ("", "none"):
224
- red_like.append(i)
225
- else:
226
- plain.append(i) # e.g. transmitted-light PMT
227
  if blue_like:
228
  myo_idx = blue_like[0]
229
- if red_like:
230
- nf_idx = red_like[0]
 
 
 
231
 
232
  # Fall back to image content when metadata is missing (e.g. plain TIFF).
233
  # Fluorescence channels have a mostly-dark background; transmitted-light
@@ -270,18 +481,89 @@ def _threshold_volume(vol: np.ndarray, sensitivity: float) -> np.ndarray:
270
  return sm > thr
271
 
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  def trace_neurites(nf_vol: np.ndarray, voxel: tuple,
274
  sensitivity: float = 1.0,
275
- min_object_vox: int = 64) -> TraceResult:
276
- """Segment and skeletonise the neurofilament network in 3D."""
 
 
 
 
 
277
  dz, dy, dx = voxel
278
  mask = _threshold_volume(nf_vol, sensitivity)
279
  mask = remove_small_objects(mask, min_object_vox)
280
- mask = ndi.binary_closing(mask, iterations=1)
 
 
281
  if mask.sum() == 0:
282
  z = np.zeros_like(mask)
283
  return TraceResult(mask, z, np.zeros(mask.shape, np.float32), voxel)
284
  skel = skeletonize(mask)
 
 
285
  dist = ndi.distance_transform_edt(mask, sampling=(dz, dy, dx)).astype(np.float32)
286
  return TraceResult(mask, skel, dist, voxel)
287
 
@@ -312,25 +594,358 @@ def suggest_boundary(myo_vol: np.ndarray) -> float:
312
  return centroid / prof.size
313
 
314
 
315
- def make_region_masks(shape_yx: tuple, boundary_frac: float,
316
- ihc_side: str = "low", axis: str = "Y") -> tuple:
317
- """Return (ihc_roi, ohc_roi) boolean 2D masks split by a straight line.
318
 
319
- axis="Y" splits along rows (radial axis, the usual case); axis="X" splits
320
- along columns. ihc_side selects which side of the boundary is IHC.
 
 
 
 
 
 
 
321
  """
322
  ny, nx = shape_yx
323
- low = np.zeros((ny, nx), bool)
324
  if axis.upper() == "Y":
325
- b = int(round(np.clip(boundary_frac, 0, 1) * ny))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  low[:b] = True
327
- else:
328
- b = int(round(np.clip(boundary_frac, 0, 1) * nx))
329
- low[:, :b] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  ihc = low if ihc_side == "low" else ~low
331
  return ihc, ~ihc
332
 
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  # --------------------------------------------------------------------------- #
335
  # Quantification
336
  # --------------------------------------------------------------------------- #
@@ -343,50 +958,166 @@ def _branch_point_count(skel: np.ndarray) -> int:
343
  return int((skel & (nb > 2)).sum())
344
 
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  def compute_metrics(trace: TraceResult, region_name: str,
347
  roi_yx: Optional[np.ndarray] = None,
348
- min_fiber_um: float = 5.0) -> RegionMetrics:
349
- """Quantify the skeleton, optionally restricted to a 2D ROI (broadcast in Z)."""
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  dz, dy, dx = trace.voxel
351
  skel = trace.skeleton
352
  mask = trace.mask
353
  if roi_yx is not None:
354
- roi3d = np.broadcast_to(roi_yx, skel.shape)
355
  skel = skel & roi3d
356
  mask = mask & roi3d
357
 
358
  m = RegionMetrics(region=region_name)
359
- m.fov_area_um2 = float(roi_yx.sum() if roi_yx is not None
360
- else mask.shape[1] * mask.shape[2]) * dx * dy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
  foot = mask.max(0)
363
  m.area_covered_um2 = float(foot.sum()) * dx * dy
364
  m.pct_area_covered = (100.0 * m.area_covered_um2 / m.fov_area_um2
365
  if m.fov_area_um2 else 0.0)
366
 
367
- if skel.sum() < 2:
368
- return m
369
-
370
- m.n_branch_points = _branch_point_count(skel)
371
-
372
- diam = 2.0 * trace.distance_um[skel]
373
- if diam.size:
374
- m.mean_diameter_um = float(diam.mean())
375
- m.median_diameter_um = float(np.median(diam))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- # Length and fiber count via skan (per connected skeleton component).
378
- try:
379
- S = Skeleton(skel, spacing=(dz, dy, dx))
380
- df = summarize(S, separator="_")
381
- comp_len = df.groupby("skeleton_id")["branch_distance"].sum()
382
- kept = comp_len[comp_len >= min_fiber_um]
383
- m.n_fibers = int(kept.size)
384
- m.total_length_um = float(kept.sum())
385
- except Exception:
386
- # Fallback: label components, approximate length by voxel count.
387
- lbl, n = ndi.label(skel, structure=np.ones((3, 3, 3)))
388
- m.n_fibers = int(n)
389
- m.total_length_um = float(skel.sum()) * np.mean([dz, dy, dx])
390
  return m
391
 
392
 
@@ -404,26 +1135,44 @@ def skeleton_image(skel: np.ndarray, dilate: int = 1) -> np.ndarray:
404
 
405
  def region_overlay(skel: np.ndarray, ihc_roi: np.ndarray, ohc_roi: np.ndarray,
406
  boundary_frac: float, axis: str = "Y",
 
407
  dilate: int = 1) -> np.ndarray:
408
- """Colour-coded RGB preview: IHC fibers cyan, OHC fibers magenta,
409
- with the boundary line drawn in yellow."""
410
- flat = skel.max(0) if skel.ndim == 3 else skel
 
 
 
 
 
 
 
 
 
 
 
411
  if dilate:
412
- flat = ndi.binary_dilation(flat, iterations=dilate)
413
- ny, nx = flat.shape
414
  rgb = np.zeros((ny, nx, 3), np.uint8)
415
- ihc_pix = flat & ihc_roi
416
- ohc_pix = flat & ohc_roi
417
  rgb[ihc_pix] = (0, 220, 255) # cyan = IHC
418
  rgb[ohc_pix] = (255, 60, 200) # magenta = OHC
419
- if axis.upper() == "Y":
420
- b = int(round(np.clip(boundary_frac, 0, 1) * ny))
421
- b = min(max(b, 0), ny - 1)
422
- rgb[b, :] = (255, 255, 0)
423
- else:
424
- b = int(round(np.clip(boundary_frac, 0, 1) * nx))
425
- b = min(max(b, 0), nx - 1)
426
- rgb[:, b] = (255, 255, 0)
 
 
 
 
 
 
 
 
427
  return rgb
428
 
429
 
 
25
  from skimage.morphology import remove_small_objects, skeletonize
26
  from skan import Skeleton, summarize
27
 
28
+
29
+ def _cellpose_available() -> bool:
30
+ try:
31
+ import cellpose # noqa: F401
32
+ return True
33
+ except Exception:
34
+ return False
35
+
36
+
37
+ CELLPOSE_AVAILABLE = _cellpose_available()
38
+ _CP_MODEL = None # lazily-created, cached Cellpose model
39
+
40
  # --------------------------------------------------------------------------- #
41
  # Data containers
42
  # --------------------------------------------------------------------------- #
 
71
  area_covered_um2: float = 0.0
72
  fov_area_um2: float = 0.0
73
  pct_area_covered: float = 0.0
74
+ n_hair_cells: int = -1 # -1 = not measured (no Myo7a detection run)
75
+ fibers_per_hc: float = -1.0 # -1 = not applicable (hair-cell count unknown)
76
+ length_per_hc_um: float = -1.0
77
+ n_radial: int = -1 # fibers running IHC->OHC (radial); -1 = n/a
78
+ n_offaxis: int = -1 # fibers running along the rows (misdirected)
79
+ pct_radial: float = -1.0 # % of fibers that are radially directed
80
 
81
  def as_row(self) -> dict:
82
+ def opt(value, ndigits=2):
83
+ """Blank when the metric was not applicable (negative sentinel)."""
84
+ return round(value, ndigits) if value is not None and value >= 0 else ""
85
+
86
+ row = {
87
  "Region": self.region,
88
  "Number of fibers": self.n_fibers,
89
+ "Hair cells (Myo7a)": (self.n_hair_cells
90
+ if self.n_hair_cells >= 0 else ""),
91
+ "Fibers / hair cell": opt(self.fibers_per_hc, 3),
92
  "Total length (um)": round(self.total_length_um, 2),
93
+ "Length / hair cell (um)": opt(self.length_per_hc_um, 2),
94
  "Mean diameter (um)": round(self.mean_diameter_um, 3),
95
  "Median diameter (um)": round(self.median_diameter_um, 3),
96
  "Branch points": self.n_branch_points,
97
+ "Radial fibers (IHC->OHC)": (self.n_radial
98
+ if self.n_radial >= 0 else ""),
99
+ "Off-axis fibers": (self.n_offaxis if self.n_offaxis >= 0 else ""),
100
+ "Radial fibers (%)": opt(self.pct_radial, 1),
101
  "Area covered (um^2)": round(self.area_covered_um2, 2),
102
  "FOV area (um^2)": round(self.fov_area_um2, 2),
103
  "Area covered (% of FOV)": round(self.pct_area_covered, 2),
104
  }
105
+ return row
106
 
107
 
108
  @dataclass
 
196
  return LoadedImage(arr, channels, _czi_voxel(czi), os.path.basename(path))
197
 
198
 
199
+ def _reorder_tiff_by_axes(arr: np.ndarray, axes: str):
200
+ """Reorder a TIFF array to (C, Z, Y, X) using tifffile's axes string.
201
+
202
+ Treats colour samples ('S', e.g. RGB) and channels ('C') as channels, keeps
203
+ 'Z', and squeezes any other axes (T, etc.). Returns None if it cannot map.
204
+ """
205
+ axes = axes.upper()
206
+ if "Y" not in axes or "X" not in axes or len(axes) != arr.ndim:
207
+ return None
208
+ # Rank: channel-like first, then Z, then Y, X; unknown axes go last.
209
+ prio = {"C": 0, "S": 0, "Z": 1, "Y": 2, "X": 3}
210
+ order = sorted(range(len(axes)), key=lambda i: (prio.get(axes[i], 4), i))
211
+ arr = np.transpose(arr, order)
212
+ new_axes = "".join(axes[i] for i in order)
213
+ # Collapse all leading channel-like axes (C and/or S) into one channel axis.
214
+ n_chan = sum(1 for c in new_axes if c in ("C", "S"))
215
+ if n_chan >= 1:
216
+ arr = arr.reshape((int(np.prod(arr.shape[:n_chan])),) + arr.shape[n_chan:])
217
+ new_axes = "C" + new_axes[n_chan:]
218
+ else:
219
+ arr = arr[None]
220
+ new_axes = "C" + new_axes
221
+ if "Z" not in new_axes: # single-plane image -> add Z=1
222
+ arr = arr[:, None]
223
+ new_axes = new_axes[0] + "Z" + new_axes[1:]
224
+ while arr.ndim > 4: # drop any leftover trailing axes
225
+ arr = arr[..., 0]
226
+ return arr
227
+
228
+
229
  def load_tiff(path: str, dz=0.35, dy=0.0895, dx=0.0895) -> LoadedImage:
230
  import tifffile
231
 
232
+ with tifffile.TiffFile(path) as tf:
233
+ arr = tf.asarray()
234
+ try:
235
+ axes = tf.series[0].axes
236
+ except Exception:
237
+ axes = None
238
+
239
+ arr = np.asarray(arr)
240
+ reordered = _reorder_tiff_by_axes(arr, axes) if axes else None
241
+
242
+ if reordered is not None:
243
+ arr = reordered
 
 
 
 
 
 
244
  else:
245
+ # Fallback heuristic when axis metadata is missing/unusable.
246
+ arr = np.squeeze(arr)
247
+ if arr.ndim == 2: # (Y, X)
248
+ arr = arr[None, None]
249
+ elif arr.ndim == 3:
250
+ # trailing RGB/RGBA samples -> channels; else small first axis = C
251
+ if arr.shape[-1] in (3, 4) and arr.shape[-1] < min(arr.shape[:2]):
252
+ arr = np.moveaxis(arr, -1, 0)[:, None] # (C, 1, Y, X)
253
+ elif arr.shape[0] <= 5:
254
+ arr = arr[:, None] # (C, 1, Y, X)
255
+ else:
256
+ arr = arr[None] # (1, Z, Y, X)
257
+ elif arr.ndim == 4:
258
+ if arr.shape[-1] in (3, 4) and arr.shape[-1] < min(arr.shape[1:3]):
259
+ arr = np.moveaxis(arr, -1, 0) # (C, Z, Y, X)
260
+ else:
261
+ yx = sorted(range(4), key=lambda a: arr.shape[a])[-2:]
262
+ rest = [a for a in range(4) if a not in yx]
263
+ c_axis = min(rest, key=lambda a: arr.shape[a])
264
+ z_axis = [a for a in rest if a != c_axis][0]
265
+ arr = np.transpose(arr, (c_axis, z_axis, *sorted(yx)))
266
+ else:
267
+ raise ValueError(f"Unsupported TIFF with {arr.ndim} dimensions")
268
+
269
+ arr = np.ascontiguousarray(arr).astype(np.float32)
270
  channels = [{"name": f"Channel {i}", "dye": None, "color": None}
271
  for i in range(arr.shape[0])]
272
  return LoadedImage(arr, channels, (dz, dy, dx), os.path.basename(path))
 
281
  raise ValueError(f"Unsupported file type: {ext}")
282
 
283
 
284
+ # --------------------------------------------------------------------------- #
285
+ # IMARIS ground-truth ingestion
286
+ # --------------------------------------------------------------------------- #
287
+
288
+ def read_imaris_filament_length(source: str) -> Optional[float]:
289
+ """Return IMARIS 'Filament Length (sum)' in microns for an image, or None.
290
+
291
+ IMARIS exports this exact statistic to
292
+ ``<name>_Statistics/<name>_Filament_Length_(sum).csv`` beside the image.
293
+ ``source`` may be that CSV, a ``_Statistics`` folder, or the image path
294
+ (``.czi``/``.tif``) whose sibling ``_Statistics`` folder is searched. When a
295
+ filament CSV lists several filament objects the per-object sums are added.
296
+
297
+ This is the ONLY way to reproduce IMARIS's exact number — it reads IMARIS's
298
+ own output rather than re-estimating it with a different algorithm.
299
+ """
300
+ import glob
301
+ import re
302
+
303
+ csvs = []
304
+ if source and os.path.isfile(source) and source.lower().endswith(".csv"):
305
+ csvs = [source]
306
+ else:
307
+ cand = None
308
+ if source and source.lower().endswith((".czi", ".tif", ".tiff")):
309
+ stem = os.path.splitext(os.path.basename(source))[0]
310
+ cand = os.path.join(os.path.dirname(source), f"{stem}_Statistics")
311
+ elif source and os.path.isdir(source):
312
+ cand = source
313
+ if cand and os.path.isdir(cand):
314
+ csvs = sorted(glob.glob(
315
+ os.path.join(cand, "*_Filament_Length_(sum).csv")))
316
+ if not csvs:
317
+ return None
318
+
319
+ total, found = 0.0, False
320
+ for csv in csvs[:1]: # one filament CSV per image
321
+ try:
322
+ with open(csv, errors="ignore") as fh:
323
+ for line in fh:
324
+ m = re.match(r"\s*([-+]?[0-9]*\.?[0-9]+)\s*,\s*µm", line)
325
+ if m:
326
+ total += float(m.group(1))
327
+ found = True
328
+ except OSError:
329
+ return None
330
+ return round(total, 3) if found else None
331
+
332
+
333
  # --------------------------------------------------------------------------- #
334
  # Channel identification
335
  # --------------------------------------------------------------------------- #
336
 
337
+ # Common fluorophores in these cochlear stacks. Each entry maps a token that
338
+ # may appear in the CZI dye/fluor name to a friendly name and a colour family.
339
+ # Emission colour drives the family; the family drives auto channel assignment.
340
+ # blue -> reference marker (Myo7a is Alexa-405 here)
341
+ # orange -> neurofilament (Alexa-555 here) — preferred trace channel
342
+ # green / red -> additional markers, selectable in the UI.
343
+ KNOWN_DYES = [
344
+ ("atto 390", "ATTO 390", "blue"),
345
+ ("405", "Alexa Fluor 405", "blue"),
346
+ ("dapi", "DAPI", "blue"),
347
+ ("488", "Alexa Fluor 488", "green"),
348
+ ("atto 488", "ATTO 488", "green"),
349
+ ("fitc", "FITC", "green"),
350
+ ("gfp", "GFP", "green"),
351
+ ("514", "Alexa Fluor 514", "green"),
352
+ ("532", "Alexa Fluor 532", "orange"),
353
+ ("546", "Alexa Fluor 546", "orange"),
354
+ ("555", "Alexa Fluor 555", "orange"),
355
+ ("568", "Alexa Fluor 568", "orange"),
356
+ ("cy3", "Cy3", "orange"),
357
+ ("594", "Alexa Fluor 594", "red"),
358
+ ("633", "Alexa Fluor 633", "red"),
359
+ ("647", "Alexa Fluor 647", "red"),
360
+ ("atto 647", "ATTO 647", "red"),
361
+ ("cy5", "Cy5", "red"),
362
+ ("680", "Alexa Fluor 680", "red"),
363
+ ]
364
+
365
+
366
+ def _channel_color_family(ch: dict) -> str:
367
+ """Classify a channel as blue / green / orange / red / none (transmitted).
368
+
369
+ Uses the dye/fluor name first, then the excitation wavelength, then the
370
+ metadata colour swatch. This lets the app recognise many more dyes
371
+ (405/488/514/532/555/568/594/633/647/ATTO/Cy…) than just 405 vs "any dye".
372
+ """
373
+ dye = (ch.get("dye") or "").lower()
374
+ color = (ch.get("color") or "").upper().lstrip("#")
375
+ ex = ch.get("ex")
376
+ try:
377
+ ex = float(ex) if ex else None
378
+ except (TypeError, ValueError):
379
+ ex = None
380
+
381
+ for token, _name, family in KNOWN_DYES:
382
+ if token in dye:
383
+ return family
384
+ if ex is not None:
385
+ if ex < 430:
386
+ return "blue"
387
+ if ex < 505:
388
+ return "green"
389
+ if ex < 565:
390
+ return "orange"
391
+ return "red"
392
+ # metadata colour swatch (Zeiss stores ARGB or RGB hex)
393
+ if color:
394
+ hexrgb = color[-6:] if len(color) >= 6 else color
395
+ try:
396
+ r = int(hexrgb[0:2], 16); g = int(hexrgb[2:4], 16); b = int(hexrgb[4:6], 16)
397
+ if b > r and b > g:
398
+ return "blue"
399
+ if g > r and g > b:
400
+ return "green"
401
+ if r > g and r > b and g > b // 2:
402
+ return "orange"
403
+ if r > g and r > b:
404
+ return "red"
405
+ except (ValueError, IndexError):
406
+ pass
407
+ if dye and dye not in ("", "none"):
408
+ return "other"
409
+ return "none" # e.g. transmitted-light PMT
410
+
411
+
412
+ def channel_dye_label(ch: dict) -> str:
413
+ """Friendly dye label for the channel dropdown (recognises known dyes)."""
414
+ dye = (ch.get("dye") or "").strip()
415
+ if dye:
416
+ low = dye.lower()
417
+ for token, name, _family in KNOWN_DYES:
418
+ if token in low:
419
+ return name
420
+ return dye
421
+ fam = _channel_color_family(ch)
422
+ return "no dye / transmitted" if fam == "none" else f"{fam} marker"
423
+
424
+
425
  def guess_channels(img: LoadedImage) -> tuple:
426
  """Best-effort (neurofilament_index, myo7a_index) from metadata + content."""
427
  nf_idx, myo_idx = None, None
428
+ families = [_channel_color_family(ch) for ch in img.channels]
429
+ blue_like = [i for i, f in enumerate(families) if f == "blue"]
430
+ orange_like = [i for i, f in enumerate(families) if f == "orange"]
431
+ red_like = [i for i, f in enumerate(families) if f == "red"]
432
+ green_like = [i for i, f in enumerate(families) if f == "green"]
433
+
434
+ # Myo7a reference = the bluest (405-like) fluorescent channel.
 
 
 
 
 
435
  if blue_like:
436
  myo_idx = blue_like[0]
437
+ # Neurofilament = Alexa-555 (orange) here; then fall back to red/green.
438
+ for group in (orange_like, red_like, green_like):
439
+ if group:
440
+ nf_idx = next((i for i in group if i != myo_idx), group[0])
441
+ break
442
 
443
  # Fall back to image content when metadata is missing (e.g. plain TIFF).
444
  # Fluorescence channels have a mostly-dark background; transmitted-light
 
481
  return sm > thr
482
 
483
 
484
+ def _remove_small_components(skel: np.ndarray, voxel: tuple,
485
+ min_len_um: float) -> np.ndarray:
486
+ """Drop connected skeleton components shorter than ``min_len_um``."""
487
+ if min_len_um <= 0 or skel.sum() == 0:
488
+ return skel
489
+ dz, dy, dx = voxel
490
+ step = float(np.mean([dy, dx])) # ~ length of one skeleton step
491
+ min_vox = max(2, int(round(min_len_um / step)))
492
+ struct = np.ones((3,) * skel.ndim, int)
493
+ lbl, n = ndi.label(skel, structure=struct)
494
+ if n == 0:
495
+ return skel
496
+ counts = np.bincount(lbl.ravel())
497
+ keep = np.zeros(counts.size, bool)
498
+ keep[1:] = counts[1:] >= min_vox
499
+ return keep[lbl]
500
+
501
+
502
+ def prune_skeleton(skel: np.ndarray, voxel: tuple,
503
+ spur_um: float = 3.0, min_component_um: Optional[float] = None,
504
+ iterations: int = 4) -> np.ndarray:
505
+ """Remove short terminal spurs and small isolated fragments.
506
+
507
+ Terminal twigs (junction→endpoint branches) shorter than ``spur_um`` are
508
+ trimmed while their junction pixel is preserved, so the backbone stays
509
+ connected. Isolated components shorter than ``min_component_um`` are dropped
510
+ entirely. Iterated a few times because trimming one spur can expose another.
511
+ """
512
+ if spur_um <= 0 and (min_component_um or 0) <= 0:
513
+ return skel
514
+ if min_component_um is None:
515
+ min_component_um = spur_um
516
+ cur = skel.copy()
517
+ for _ in range(iterations):
518
+ if cur.sum() == 0 or spur_um <= 0:
519
+ break
520
+ try:
521
+ S = Skeleton(cur, spacing=voxel)
522
+ df = summarize(S, separator="_")
523
+ except Exception:
524
+ break
525
+ k = np.ones((3,) * cur.ndim, int)
526
+ nb = ndi.convolve(cur.astype(np.uint8), k, mode="constant") - cur
527
+ junction = cur & (nb > 2) # protect branch points
528
+ drop = df.index[
529
+ ((df["branch_type"] == 1) & (df["branch_distance"] < spur_um)) |
530
+ (df["branch_type"].isin([0, 3]) &
531
+ (df["branch_distance"] < min_component_um))]
532
+ if len(drop) == 0:
533
+ break
534
+ removal = np.zeros_like(cur)
535
+ for i in drop:
536
+ coords = S.path_coordinates(i).astype(int)
537
+ removal[tuple(coords.T)] = True
538
+ removal &= ~junction
539
+ new = cur & ~removal
540
+ if int(new.sum()) == int(cur.sum()):
541
+ break
542
+ cur = new
543
+ return _remove_small_components(cur, voxel, min_component_um)
544
+
545
+
546
  def trace_neurites(nf_vol: np.ndarray, voxel: tuple,
547
  sensitivity: float = 1.0,
548
+ min_object_vox: int = 64,
549
+ prune_um: float = 3.0) -> TraceResult:
550
+ """Segment and skeletonise the neurofilament network in 3D.
551
+
552
+ ``prune_um`` removes terminal spurs and isolated fragments shorter than this
553
+ many microns from the skeleton, giving cleaner fiber lines (0 = no pruning).
554
+ """
555
  dz, dy, dx = voxel
556
  mask = _threshold_volume(nf_vol, sensitivity)
557
  mask = remove_small_objects(mask, min_object_vox)
558
+ # Close within each z-plane. A full 3D element would erode a single-plane
559
+ # (Z=1) stack to nothing, since its z-neighbours fall outside the array.
560
+ mask = ndi.binary_closing(mask, structure=np.ones((1, 3, 3), bool))
561
  if mask.sum() == 0:
562
  z = np.zeros_like(mask)
563
  return TraceResult(mask, z, np.zeros(mask.shape, np.float32), voxel)
564
  skel = skeletonize(mask)
565
+ if prune_um and prune_um > 0:
566
+ skel = prune_skeleton(skel, voxel, spur_um=float(prune_um))
567
  dist = ndi.distance_transform_edt(mask, sampling=(dz, dy, dx)).astype(np.float32)
568
  return TraceResult(mask, skel, dist, voxel)
569
 
 
594
  return centroid / prof.size
595
 
596
 
597
+ RADIAL_AXIS_INDEX = {"Z": 0, "Y": 1, "X": 2}
598
+
 
599
 
600
+ def boundary_line_coords(shape_yx: tuple, boundary_frac: float, axis: str = "Y",
601
+ angle_deg: float = 0.0, curvature: float = 0.0) -> tuple:
602
+ """Pixel coordinates of the (possibly tilted / curved) boundary line.
603
+
604
+ Returns (ys, xs) integer arrays to index a 2D image, so the same line is
605
+ drawn on every overlay and used to build the region masks — they can never
606
+ disagree. ``angle_deg`` tilts the line; ``curvature`` bows it (a parabola,
607
+ positive = bow toward higher coordinates in the middle). For axis="Z" there
608
+ is no in-plane line, so empty arrays are returned.
609
  """
610
  ny, nx = shape_yx
 
611
  if axis.upper() == "Y":
612
+ xs = np.arange(nx)
613
+ cx, hx = nx / 2.0, max(nx / 2.0, 1.0)
614
+ ys = (boundary_frac * ny + np.tan(np.deg2rad(angle_deg)) * (xs - cx)
615
+ + curvature * ((xs - cx) / hx) ** 2 * ny)
616
+ ys = np.clip(np.round(ys), 0, ny - 1).astype(int)
617
+ return ys, xs
618
+ if axis.upper() == "X":
619
+ ys = np.arange(ny)
620
+ cy, hy = ny / 2.0, max(ny / 2.0, 1.0)
621
+ xs = (boundary_frac * nx + np.tan(np.deg2rad(angle_deg)) * (ys - cy)
622
+ + curvature * ((ys - cy) / hy) ** 2 * nx)
623
+ xs = np.clip(np.round(xs), 0, nx - 1).astype(int)
624
+ return ys, xs
625
+ return np.array([], int), np.array([], int) # axis == "Z"
626
+
627
+
628
+ def make_region_masks(shape: tuple, boundary_frac: float,
629
+ ihc_side: str = "low", axis: str = "Y",
630
+ angle_deg: float = 0.0, curvature: float = 0.0) -> tuple:
631
+ """Return (ihc_roi, ohc_roi) boolean masks split by the boundary.
632
+
633
+ ``shape`` may be (ny, nx) or (nz, ny, nx). axis="Y" splits along rows
634
+ (radial axis, the usual case), axis="X" splits along columns; both accept a
635
+ tilt (``angle_deg``) and a bow (``curvature``) and return 2D masks that are
636
+ broadcast over Z during quantification. axis="Z" splits by depth and
637
+ returns 3D masks. ``ihc_side`` selects which side ("low" = smaller
638
+ coordinate) is IHC.
639
+ """
640
+ axis = axis.upper()
641
+ if axis == "Z":
642
+ if len(shape) != 3:
643
+ raise ValueError("Z split needs a 3D (nz, ny, nx) shape")
644
+ nz, ny, nx = shape
645
+ if nz < 2:
646
+ raise ValueError("Z (depth) split needs a multi-plane stack "
647
+ "(nz >= 2); this image has a single plane.")
648
+ b = int(round(np.clip(boundary_frac, 0, 1) * nz))
649
+ low = np.zeros((nz, ny, nx), bool)
650
  low[:b] = True
651
+ ihc = low if ihc_side == "low" else ~low
652
+ return ihc, ~ihc
653
+
654
+ ny, nx = shape[-2], shape[-1]
655
+ yy, xx = np.mgrid[0:ny, 0:nx]
656
+ ang = np.tan(np.deg2rad(angle_deg))
657
+ if axis == "Y":
658
+ cx, hx = nx / 2.0, max(nx / 2.0, 1.0)
659
+ yline = (boundary_frac * ny + ang * (xx - cx)
660
+ + curvature * ((xx - cx) / hx) ** 2 * ny)
661
+ low = yy < yline
662
+ else: # axis == "X"
663
+ cy, hy = ny / 2.0, max(ny / 2.0, 1.0)
664
+ xline = (boundary_frac * nx + ang * (yy - cy)
665
+ + curvature * ((yy - cy) / hy) ** 2 * nx)
666
+ low = xx < xline
667
  ihc = low if ihc_side == "low" else ~low
668
  return ihc, ~ihc
669
 
670
 
671
+ # --------------------------------------------------------------------------- #
672
+ # Hair-cell detection (Cellpose assist, with a classical fallback)
673
+ # --------------------------------------------------------------------------- #
674
+
675
+ # A mouse cochlear hair cell body is roughly this wide; used to pick the
676
+ # working scale so cells land near Cellpose's preferred pixel size.
677
+ HAIR_CELL_DIAMETER_UM = 8.0
678
+ _CP_TARGET_PX = 30
679
+
680
+
681
+ def _norm(mip: np.ndarray) -> np.ndarray:
682
+ lo, hi = np.percentile(mip, 1), np.percentile(mip, 99.5)
683
+ return np.clip((mip - lo) / (hi - lo + 1e-6), 0, 1).astype(np.float32)
684
+
685
+
686
+ def custom_cellpose_model_path() -> Optional[str]:
687
+ """Path to a fine-tuned Cellpose hair-cell model, if one is provided.
688
+
689
+ Looked up in order:
690
+ 1. the ``NEURON_TRACER_CP_MODEL`` environment variable, then
691
+ 2. a bundled ``models/hair_cell_cpsam`` file next to this module.
692
+ Returns None to use the stock pretrained Cellpose model. This is how a
693
+ model trained with ``training/train_hair_cells.py`` gets picked up by the
694
+ app: drop it at ``models/hair_cell_cpsam`` (or point the env var at it).
695
+ """
696
+ p = os.environ.get("NEURON_TRACER_CP_MODEL")
697
+ if p and os.path.exists(p):
698
+ return p
699
+ here = os.path.dirname(os.path.abspath(__file__))
700
+ bundled = os.path.join(here, "models", "hair_cell_cpsam")
701
+ return bundled if os.path.exists(bundled) else None
702
+
703
+
704
+ def _get_cellpose_model():
705
+ global _CP_MODEL
706
+ if _CP_MODEL is None:
707
+ from cellpose import models
708
+ mp = custom_cellpose_model_path()
709
+ if mp:
710
+ _CP_MODEL = models.CellposeModel(gpu=False, pretrained_model=mp)
711
+ else:
712
+ _CP_MODEL = models.CellposeModel(gpu=False)
713
+ return _CP_MODEL
714
+
715
+
716
+ def prep_detection_image(myo_vol: np.ndarray, voxel: tuple) -> tuple:
717
+ """The exact Myo7a image the detector feeds to Cellpose, plus its scale.
718
+
719
+ Returns ``(img_uint8, scale)`` where ``img_uint8`` is the contrast-
720
+ normalised max-projection rescaled so hair cells sit near Cellpose's
721
+ working size, and ``scale`` maps its coordinates back to full resolution
722
+ (full = detected / scale). Exposed so ``training/prepare_data.py`` can
723
+ export annotation images at the *same* scale the model sees at inference —
724
+ keeping training and inference consistent.
725
+ """
726
+ dx = voxel[2]
727
+ mip = _norm(myo_vol.max(0).astype(np.float32))
728
+ cell_px_full = HAIR_CELL_DIAMETER_UM / dx # e.g. ~89 px
729
+ scale = float(np.clip(_CP_TARGET_PX / cell_px_full, 0.2, 1.0))
730
+ from skimage.transform import rescale
731
+ small = (rescale(mip, scale, anti_aliasing=True, preserve_range=True
732
+ ).astype(np.float32) if scale < 0.999 else mip)
733
+ small = _norm(small)
734
+ return (small * 255).astype(np.uint8), scale
735
+
736
+
737
+ def _watershed_cells(img: np.ndarray, cell_px: float) -> np.ndarray:
738
+ """Classical blob segmentation fallback (no deep-learning dependency)."""
739
+ from skimage.feature import peak_local_max
740
+ from skimage.segmentation import watershed
741
+ sm = gaussian(img, max(cell_px / 6.0, 1.0), preserve_range=True)
742
+ try:
743
+ mask = sm > threshold_otsu(sm)
744
+ except Exception:
745
+ return np.zeros(img.shape, int)
746
+ mask = ndi.binary_opening(mask, iterations=1)
747
+ dist = ndi.distance_transform_edt(mask)
748
+ coords = peak_local_max(dist, min_distance=max(int(cell_px * 0.5), 3),
749
+ labels=mask)
750
+ if len(coords) == 0:
751
+ return np.zeros(img.shape, int)
752
+ markers = np.zeros(img.shape, int)
753
+ markers[tuple(coords.T)] = np.arange(1, len(coords) + 1)
754
+ return watershed(-dist, markers, mask=mask)
755
+
756
+
757
+ def detect_hair_cells(myo_vol: np.ndarray, voxel: tuple,
758
+ prefer_cellpose: bool = True) -> dict:
759
+ """Detect Myo7a hair-cell bodies on the max-projection.
760
+
761
+ Uses Cellpose when available (better on touching cells), otherwise a
762
+ watershed fallback. If a fine-tuned model is provided (see
763
+ ``custom_cellpose_model_path``) it is used automatically and the engine is
764
+ reported as ``cellpose-custom``. Returns full-resolution centroids plus the
765
+ count and which engine ran. The image is rescaled so cells are ~30 px,
766
+ which is where Cellpose performs best.
767
+ """
768
+ mip_shape = myo_vol.max(0).shape
769
+ small, scale = prep_detection_image(myo_vol, voxel)
770
+ small = small.astype(np.float32) / 255.0
771
+
772
+ engine = "watershed"
773
+ masks = None
774
+ if prefer_cellpose and CELLPOSE_AVAILABLE:
775
+ try:
776
+ masks = _get_cellpose_model().eval(small, diameter=_CP_TARGET_PX)[0]
777
+ engine = ("cellpose-custom" if custom_cellpose_model_path()
778
+ else "cellpose")
779
+ except Exception:
780
+ masks = None
781
+ if masks is None:
782
+ masks = _watershed_cells(small, _CP_TARGET_PX)
783
+
784
+ n = int(masks.max())
785
+ if n:
786
+ cent_small = np.array(ndi.center_of_mass(
787
+ np.ones_like(masks), masks, range(1, n + 1)))
788
+ centroids = cent_small / scale # back to full-res Y,X
789
+ else:
790
+ centroids = np.zeros((0, 2))
791
+ return {"centroids": centroids, "count": n, "engine": engine,
792
+ "scale": scale, "mip_shape": mip_shape}
793
+
794
+
795
+ # Anatomy-based thresholds for accepting an IHC/OHC split (mouse organ of Corti).
796
+ _MIN_TWO_ROW_EXTENT_UM = 18.0 # IHC row + tunnel + OHC rows span well past this;
797
+ # a single hair-cell row spans < ~15 µm radially.
798
+ _MIN_TUNNEL_UM = 7.0 # the IHC/OHC gap (tunnel of Corti) is ~10-25 µm;
799
+ # inter-cell spacing within a row is smaller.
800
+
801
+
802
+ def auto_regions_from_cells(centroids: np.ndarray, shape_yx: tuple,
803
+ um_per_px: Optional[float] = None) -> dict:
804
+ """Suggest an IHC/OHC boundary from hair-cell centroids.
805
+
806
+ IHCs form a single row and OHCs form three rows separated from the IHCs by
807
+ the tunnel of Corti. We project cells onto the radial axis (perpendicular
808
+ to the hair-cell band), find the widest cell-free gap that leaves at least
809
+ two cells on each side, and call the sparser/tighter side IHC.
810
+
811
+ Crucially, a split is only proposed when the cells actually span more than
812
+ one row (radial extent) *and* the gap is a real cell-free tunnel. When only
813
+ a single hair-cell row is present, ``single_row`` is returned True and no
814
+ split is made — the field should be reported whole-field, not forced into a
815
+ meaningless IHC/OHC division.
816
+ """
817
+ ny, nx = shape_yx
818
+ result = {"boundary_frac": 0.5, "ihc_side": "low", "confidence": "low",
819
+ "n_ihc_cells": 0, "n_ohc_cells": 0, "single_row": False,
820
+ "reason": "not enough hair cells for an automatic split"}
821
+ if len(centroids):
822
+ result["boundary_frac"] = float(np.clip(centroids[:, 0].mean() / ny, 0, 1))
823
+ if len(centroids) < 6:
824
+ result["single_row"] = True
825
+ result["reason"] = (f"only {len(centroids)} hair cell(s) detected — "
826
+ "cannot separate IHC vs OHC; report whole-field")
827
+ return result
828
+
829
+ c = centroids - centroids.mean(0)
830
+ _, _, vt = np.linalg.svd(c, full_matrices=False)
831
+ radial = vt[1]
832
+ if radial[0] < 0:
833
+ radial = -radial # point toward +Y
834
+ r = c @ radial
835
+ order = np.argsort(r)
836
+ rs = r[order]
837
+ gaps = np.diff(rs)
838
+
839
+ # Convert radial distances to microns (or fall back to image-fraction units).
840
+ if um_per_px and um_per_px > 0:
841
+ to_um = um_per_px
842
+ extent = (rs[-1] - rs[0]) * to_um
843
+ min_extent, min_gap = _MIN_TWO_ROW_EXTENT_UM, _MIN_TUNNEL_UM
844
+ unit = "µm"
845
+ else: # unknown scale
846
+ to_um = 1.0
847
+ extent = (rs[-1] - rs[0]) / max(ny, 1) # fraction of image
848
+ min_extent, min_gap = 0.12, 0.04
849
+ unit = "frac"
850
+
851
+ # Candidate splits must leave >=2 cells on each side (ignore stray outliers).
852
+ valid = [(i, gaps[i]) for i in range(1, len(gaps) - 1)]
853
+ biggest_gap = (max(valid, key=lambda t: t[1])[1] * to_um) if valid else 0.0
854
+
855
+ # Single-row / no-tunnel guard: refuse to split an unresolved band.
856
+ if extent < min_extent or not valid or biggest_gap < min_gap:
857
+ result["single_row"] = True
858
+ result["reason"] = (
859
+ f"hair cells span only {extent:.1f} {unit} radially with no clear "
860
+ f"tunnel gap — looks like a single row; IHC vs OHC cannot be "
861
+ f"separated from this field. Report whole-field or set the boundary "
862
+ f"by hand if you know the anatomy.")
863
+ return result
864
+
865
+ gi = max(valid, key=lambda t: t[1])[0]
866
+ split_r = (rs[gi] + rs[gi + 1]) / 2.0
867
+
868
+ left = r <= split_r
869
+ n_left, n_right = int(left.sum()), int((~left).sum())
870
+ s_left = float(r[left].std()) if n_left > 1 else 0.0
871
+ s_right = float(r[~left].std()) if n_right > 1 else 0.0
872
+ # IHC = single row: fewer cells and tighter spread
873
+ score = (1 if n_right > n_left else -1) + (1 if s_right > s_left else -1)
874
+ ihc_is_left = score >= 0
875
+ ihc_side = "low" if ihc_is_left else "high"
876
+ n_ihc = n_left if ihc_is_left else n_right
877
+ n_ohc = n_right if ihc_is_left else n_left
878
+
879
+ ymid = centroids[:, 0].mean() + split_r * radial[0]
880
+ bfrac = float(np.clip(ymid / ny, 0, 1))
881
+
882
+ ratio = n_ohc / max(n_ihc, 1)
883
+ strong_gap = (biggest_gap >= 2 * min_gap)
884
+ if strong_gap and extent >= 1.6 * min_extent and 1.8 <= ratio <= 5.0:
885
+ conf = "high"
886
+ else:
887
+ conf = "medium"
888
+
889
+ return {"boundary_frac": bfrac, "ihc_side": ihc_side, "confidence": conf,
890
+ "n_ihc_cells": n_ihc, "n_ohc_cells": n_ohc, "single_row": False,
891
+ "reason": f"tunnel gap {biggest_gap:.1f} {unit}, radial extent "
892
+ f"{extent:.1f} {unit}, IHC:OHC cell ratio 1:{ratio:.1f}"}
893
+
894
+
895
+ def count_cells_in_roi(centroids: np.ndarray, roi_yx: np.ndarray) -> int:
896
+ """Count hair-cell centroids falling inside a 2D ROI mask."""
897
+ if len(centroids) == 0:
898
+ return 0
899
+ ny, nx = roi_yx.shape
900
+ yy = np.clip(centroids[:, 0].round().astype(int), 0, ny - 1)
901
+ xx = np.clip(centroids[:, 1].round().astype(int), 0, nx - 1)
902
+ return int(roi_yx[yy, xx].sum())
903
+
904
+
905
+ def _draw_boundary_rgb(rgb: np.ndarray, boundary_frac: Optional[float],
906
+ axis: str = "Y", angle_deg: float = 0.0,
907
+ curvature: float = 0.0, color=(255, 255, 0)) -> np.ndarray:
908
+ """Draw the (tilted / curved) boundary line onto an RGB image in place.
909
+
910
+ For axis="Z" there is no in-plane line, so nothing is drawn.
911
+ """
912
+ if boundary_frac is None:
913
+ return rgb
914
+ ny, nx = rgb.shape[:2]
915
+ ys, xs = boundary_line_coords((ny, nx), boundary_frac, axis, angle_deg, curvature)
916
+ if ys.size:
917
+ rgb[ys, xs] = color
918
+ return rgb
919
+
920
+
921
+ def hair_cell_overlay(myo_mip_u8: np.ndarray, centroids: np.ndarray,
922
+ boundary_frac: Optional[float] = None,
923
+ axis: str = "Y", ihc_side: Optional[str] = None,
924
+ angle_deg: float = 0.0, curvature: float = 0.0) -> np.ndarray:
925
+ """Myo7a MIP with detected hair cells marked and the boundary drawn.
926
+
927
+ Cells are coloured by the *actual* region mask (so tilt/curvature are
928
+ honoured): cyan = IHC region, magenta = OHC region, neutral green when no
929
+ split is defined.
930
+ """
931
+ rgb = np.stack([myo_mip_u8] * 3, axis=-1).copy()
932
+ ny, nx = myo_mip_u8.shape
933
+ ihc_mask = None
934
+ if (boundary_frac is not None and ihc_side is not None
935
+ and axis.upper() in ("Y", "X")):
936
+ ihc_mask, _ = make_region_masks((ny, nx), boundary_frac, ihc_side,
937
+ axis, angle_deg, curvature)
938
+ for (y, x) in centroids.astype(int):
939
+ yc, xc = min(max(y, 0), ny - 1), min(max(x, 0), nx - 1)
940
+ col = (0, 255, 0)
941
+ if ihc_mask is not None:
942
+ col = (0, 220, 255) if ihc_mask[yc, xc] else (255, 60, 200)
943
+ ys, xs = slice(max(0, y - 3), y + 4), slice(max(0, x - 3), x + 4)
944
+ rgb[ys, xs] = col
945
+ _draw_boundary_rgb(rgb, boundary_frac, axis, angle_deg, curvature)
946
+ return rgb
947
+
948
+
949
  # --------------------------------------------------------------------------- #
950
  # Quantification
951
  # --------------------------------------------------------------------------- #
 
958
  return int((skel & (nb > 2)).sum())
959
 
960
 
961
+ def _fiber_stats(skel: np.ndarray, distance_um: np.ndarray, voxel: tuple,
962
+ min_fiber_um: float, min_diam_um: float, max_diam_um: float,
963
+ radial_axis: Optional[int]) -> tuple:
964
+ """Per-fiber (connected-component) filtering and radial-direction counting.
965
+
966
+ A fiber is kept only if its length >= ``min_fiber_um`` **and** its mean
967
+ diameter is within [``min_diam_um``, ``max_diam_um``] (``max_diam_um`` <= 0
968
+ means no upper limit). When ``radial_axis`` is given, each kept fiber is
969
+ classified as radial (its principal axis aligns with the IHC->OHC axis) or
970
+ off-axis (running along the rows — a "misdirected" fiber).
971
+
972
+ Returns (kept_skel_bool, n_fibers, total_length_um, n_radial, n_offaxis).
973
+ ``n_radial``/``n_offaxis`` are -1 when direction was not computed.
974
+ """
975
+ struct = np.ones((3,) * skel.ndim, int)
976
+ lbl, n = ndi.label(skel, structure=struct)
977
+ if n == 0:
978
+ return np.zeros_like(skel), 0, 0.0, -1, -1
979
+
980
+ # Length per component: skan gives spacing-aware branch lengths; map each
981
+ # branch to its connected component via a pixel lying on the branch.
982
+ comp_len = {}
983
+ try:
984
+ S = Skeleton(skel, spacing=tuple(voxel))
985
+ df = summarize(S, separator="_")
986
+ for pi in df.index:
987
+ coords = np.round(S.path_coordinates(pi)).astype(int)
988
+ for a in range(coords.shape[1]):
989
+ coords[:, a] = np.clip(coords[:, a], 0, skel.shape[a] - 1)
990
+ c = 0
991
+ for p in (coords[len(coords) // 2], coords[0], coords[-1]):
992
+ c = int(lbl[tuple(p)])
993
+ if c:
994
+ break
995
+ if c:
996
+ comp_len[c] = comp_len.get(c, 0.0) + float(df.loc[pi, "branch_distance"])
997
+ except Exception:
998
+ step = float(np.mean(voxel))
999
+ for c in range(1, n + 1):
1000
+ comp_len[c] = float((lbl == c).sum()) * step
1001
+
1002
+ spacing = np.asarray(voxel, float)
1003
+ slices = ndi.find_objects(lbl)
1004
+ kept = np.zeros(n + 1, bool)
1005
+ n_radial = n_offaxis = 0
1006
+ do_dir = radial_axis is not None
1007
+ for c in range(1, n + 1):
1008
+ sl = slices[c - 1]
1009
+ if sl is None:
1010
+ continue
1011
+ # A component with no measured length is a degenerate fragment (e.g. an
1012
+ # isolated voxel skan emits no branch for) — never count it as a fiber,
1013
+ # even when min_fiber_um is 0. This also guarantees kept[c] => c in
1014
+ # comp_len, so the total_length sum below can index comp_len safely.
1015
+ if c not in comp_len or comp_len[c] < min_fiber_um:
1016
+ continue
1017
+ sub = lbl[sl] == c
1018
+ dsub = 2.0 * distance_um[sl][sub]
1019
+ mean_d = float(dsub.mean()) if dsub.size else 0.0
1020
+ if mean_d < min_diam_um or (max_diam_um > 0 and mean_d > max_diam_um):
1021
+ continue
1022
+ kept[c] = True
1023
+ if do_dir:
1024
+ offset = np.array([s.start for s in sl], float)
1025
+ pts = (np.argwhere(sub).astype(float) + offset) * spacing
1026
+ if len(pts) >= 2:
1027
+ pc = pts - pts.mean(0)
1028
+ _, _, vt = np.linalg.svd(pc, full_matrices=False)
1029
+ principal = vt[0]
1030
+ cos_r = abs(principal[radial_axis]) / (np.linalg.norm(principal) + 1e-9)
1031
+ n_radial += int(cos_r >= 0.5)
1032
+ n_offaxis += int(cos_r < 0.5)
1033
+ else:
1034
+ n_offaxis += 1
1035
+
1036
+ kept_skel = kept[lbl]
1037
+ n_fibers = int(kept[1:].sum())
1038
+ total_length = float(sum(comp_len.get(c, 0.0)
1039
+ for c in range(1, n + 1) if kept[c]))
1040
+ if not do_dir:
1041
+ n_radial = n_offaxis = -1
1042
+ return kept_skel, n_fibers, total_length, n_radial, n_offaxis
1043
+
1044
+
1045
  def compute_metrics(trace: TraceResult, region_name: str,
1046
  roi_yx: Optional[np.ndarray] = None,
1047
+ min_fiber_um: float = 5.0,
1048
+ hair_cell_centroids: Optional[np.ndarray] = None,
1049
+ min_diameter_um: float = 0.0,
1050
+ max_diameter_um: float = 0.0,
1051
+ manual_hair_cells: Optional[int] = None,
1052
+ radial_axis_name: str = "Y") -> RegionMetrics:
1053
+ """Quantify the skeleton, optionally restricted to an ROI (2D or 3D).
1054
+
1055
+ Fibers are filtered by length (``min_fiber_um``) and, when set, by mean
1056
+ diameter (``min_diameter_um`` / ``max_diameter_um``). ``manual_hair_cells``
1057
+ overrides the detected hair-cell count for normalisation; otherwise
1058
+ ``hair_cell_centroids`` (if given) are counted inside the region.
1059
+ ``radial_axis_name`` ("Y"/"X"/"Z") is the IHC->OHC axis used to classify
1060
+ fiber direction.
1061
+ """
1062
  dz, dy, dx = trace.voxel
1063
  skel = trace.skeleton
1064
  mask = trace.mask
1065
  if roi_yx is not None:
1066
+ roi3d = np.broadcast_to(roi_yx, skel.shape) if roi_yx.ndim == 2 else roi_yx
1067
  skel = skel & roi3d
1068
  mask = mask & roi3d
1069
 
1070
  m = RegionMetrics(region=region_name)
1071
+
1072
+ # 2D footprint of the ROI drives area / FOV (handles 2D and 3D ROIs).
1073
+ if roi_yx is not None:
1074
+ roi_foot = roi_yx if roi_yx.ndim == 2 else roi_yx.max(0)
1075
+ m.fov_area_um2 = float(roi_foot.sum()) * dx * dy
1076
+ else:
1077
+ roi_foot = None
1078
+ m.fov_area_um2 = float(mask.shape[1] * mask.shape[2]) * dx * dy
1079
+
1080
+ # Hair-cell count: manual override wins, else count detected centroids.
1081
+ if manual_hair_cells is not None and manual_hair_cells >= 0:
1082
+ m.n_hair_cells = int(manual_hair_cells)
1083
+ elif hair_cell_centroids is not None:
1084
+ if roi_foot is not None:
1085
+ m.n_hair_cells = count_cells_in_roi(hair_cell_centroids, roi_foot)
1086
+ else:
1087
+ m.n_hair_cells = int(len(hair_cell_centroids))
1088
 
1089
  foot = mask.max(0)
1090
  m.area_covered_um2 = float(foot.sum()) * dx * dy
1091
  m.pct_area_covered = (100.0 * m.area_covered_um2 / m.fov_area_um2
1092
  if m.fov_area_um2 else 0.0)
1093
 
1094
+ if skel.sum() >= 2:
1095
+ name = (radial_axis_name or "Y").upper()
1096
+ if skel.ndim == 3:
1097
+ radial_axis = {"Z": 0, "Y": 1, "X": 2}.get(name, 1)
1098
+ else:
1099
+ radial_axis = {"Y": 0, "X": 1}.get(name, 0)
1100
+ kept_skel, n_fibers, total_len, n_radial, n_offaxis = _fiber_stats(
1101
+ skel, trace.distance_um, (dz, dy, dx), float(min_fiber_um),
1102
+ float(min_diameter_um), float(max_diameter_um), radial_axis)
1103
+ m.n_fibers = n_fibers
1104
+ m.total_length_um = total_len
1105
+ m.n_branch_points = _branch_point_count(kept_skel)
1106
+ diam = 2.0 * trace.distance_um[kept_skel]
1107
+ if diam.size:
1108
+ m.mean_diameter_um = float(diam.mean())
1109
+ m.median_diameter_um = float(np.median(diam))
1110
+ if n_radial >= 0:
1111
+ m.n_radial = n_radial
1112
+ m.n_offaxis = n_offaxis
1113
+ tot = n_radial + n_offaxis
1114
+ m.pct_radial = (100.0 * n_radial / tot) if tot else 0.0
1115
+
1116
+ # Normalisation by hair-cell count (when known and positive).
1117
+ if m.n_hair_cells > 0:
1118
+ m.fibers_per_hc = m.n_fibers / m.n_hair_cells
1119
+ m.length_per_hc_um = m.total_length_um / m.n_hair_cells
1120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1121
  return m
1122
 
1123
 
 
1135
 
1136
  def region_overlay(skel: np.ndarray, ihc_roi: np.ndarray, ohc_roi: np.ndarray,
1137
  boundary_frac: float, axis: str = "Y",
1138
+ angle_deg: float = 0.0, curvature: float = 0.0,
1139
  dilate: int = 1) -> np.ndarray:
1140
+ """Colour-coded RGB preview: IHC fibers cyan, OHC fibers magenta, with the
1141
+ boundary line in yellow. Works with 2D (broadcast) or 3D (Z-split) ROIs."""
1142
+ if skel.ndim == 3:
1143
+ ihc3 = (np.broadcast_to(ihc_roi, skel.shape) if ihc_roi.ndim == 2
1144
+ else ihc_roi)
1145
+ ohc3 = (np.broadcast_to(ohc_roi, skel.shape) if ohc_roi.ndim == 2
1146
+ else ohc_roi)
1147
+ ihc_pix = (skel & ihc3).max(0)
1148
+ ohc_pix = (skel & ohc3).max(0)
1149
+ ny, nx = skel.shape[1], skel.shape[2]
1150
+ else:
1151
+ ihc_pix = skel & ihc_roi
1152
+ ohc_pix = skel & ohc_roi
1153
+ ny, nx = skel.shape
1154
  if dilate:
1155
+ ihc_pix = ndi.binary_dilation(ihc_pix, iterations=dilate)
1156
+ ohc_pix = ndi.binary_dilation(ohc_pix, iterations=dilate)
1157
  rgb = np.zeros((ny, nx, 3), np.uint8)
 
 
1158
  rgb[ihc_pix] = (0, 220, 255) # cyan = IHC
1159
  rgb[ohc_pix] = (255, 60, 200) # magenta = OHC
1160
+ _draw_boundary_rgb(rgb, boundary_frac, axis, angle_deg, curvature)
1161
+ return rgb
1162
+
1163
+
1164
+ def overlay_on_original(base_gray_u8: np.ndarray, skel: np.ndarray,
1165
+ color=(255, 70, 70), dilate: int = 1) -> np.ndarray:
1166
+ """Traced skeleton drawn in colour over the original (grayscale) MIP.
1167
+
1168
+ Lets the trace be checked against the raw signal — the reviewers' request
1169
+ to view the tracing overlaid on the original image.
1170
+ """
1171
+ flat = skel.max(0) if skel.ndim == 3 else skel
1172
+ if dilate:
1173
+ flat = ndi.binary_dilation(flat, iterations=dilate)
1174
+ rgb = np.stack([base_gray_u8] * 3, axis=-1).copy()
1175
+ rgb[flat] = color
1176
  return rgb
1177
 
1178
 
reference_medclipseg/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
reference_medclipseg/README.md ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+
3
+ # MedCLIPSeg: Probabilistic Vision–Language Adaptation for Data-Efficient and Generalizable Medical Image Segmentation
4
+
5
+ <h3>CVPR 2026</h3>
6
+
7
+ **[Health-X Lab](http://www.healthx-lab.ca/)** | **[IMPACT Lab](https://users.encs.concordia.ca/~impact/)**
8
+
9
+ [Taha Koleilat](https://tahakoleilat.github.io/),
10
+ [Hojat Asgariandehkordi](https://scholar.google.com/citations?user=ndXNye4AAAAJ&hl=en),
11
+ [Omid Nejati Manzari](https://omid-nejati.github.io/),
12
+ [Berardino Barile](https://scholar.google.com/citations?user=odmpMGcAAAAJ&hl=en),
13
+ [Yiming Xiao](https://yimingxiao.weebly.com/curriculum-vitae.html)<sup>†</sup>,
14
+ [Hassan Rivaz](https://users.encs.concordia.ca/~hrivaz/)<sup>†</sup>
15
+
16
+ <a href="https://arxiv.org/abs/2602.20423" target="_blank"><img alt="arXiv" src="https://img.shields.io/badge/arXiv-2602.20423-B31B1B?logo=arxiv&logoColor=white" height="25"/></a>
17
+ <a href="https://tahakoleilat.github.io/MedCLIPSeg" target="_blank"><img alt="Project Website" src="https://img.shields.io/badge/%F0%9F%94%97%20Project-Website-blue" height="25"/></a>
18
+ <a href="https://huggingface.co/datasets/TahaKoleilat/MedCLIPSeg" target="_blank"><img alt="HuggingFace Dataset" src="https://img.shields.io/badge/%F0%9F%A4%97%20_Datasets-Access-ffc107?color=ffc107&logoColor=white" height="25"/></a>
19
+ <a href="https://huggingface.co/TahaKoleilat/MedCLIPSeg" target="_blank"><img alt="HuggingFace Models" src="https://img.shields.io/badge/Models-Reproduce-2ea44f?logo=huggingface&logoColor=white" height="25"/></a>
20
+ <a href="#citation"><img alt="Citation" src="https://img.shields.io/badge/Citation-BibTeX-6C63FF?logo=bookstack&logoColor=white" height="25"/></a>
21
+
22
+ † *Co-senior authors*
23
+ </div>
24
+
25
+ ## Overview
26
+
27
+ ![main figure](assets/overview.png)
28
+ > **<p align="justify"> Abstract:** *Medical image segmentation remains challenging due to limited annotations for training, ambiguous anatomical features, and domain shifts. While vision–language models such as **CLIP** offer strong cross-modal representations, their potential for dense, text-guided medical image segmentation remains underexplored. We present **MedCLIPSeg**, a novel framework that adapts CLIP for **robust, data-efficient, and uncertainty-aware** medical image segmentation. Our approach leverages **patch-level CLIP embeddings** through **probabilistic cross-modal attention**, enabling bidirectional interaction between image and text tokens and explicit modeling of predictive uncertainty. Together with a **soft patch-level contrastive loss** that encourages nuanced semantic learning across diverse textual prompts, **MedCLIPSeg** improves data efficiency and domain generalizability. Extensive experiments across **16 datasets**, spanning **five imaging modalities** and **six organs**, demonstrate that **MedCLIPSeg** outperforms prior methods in **accuracy, efficiency, and robustness**, while providing **interpretable uncertainty maps** that highlight the local reliability of segmentation results. This work demonstrates the potential of **probabilistic vision–language modeling** for text-driven medical image segmentation.* </p>
29
+
30
+ ## Method
31
+
32
+ <p align="center">
33
+ <img src="assets/MedCLIPSeg.png" width="100%" />
34
+ <br>
35
+ <em>Overall architecture of MedCLIPSeg. The framework integrates probabilistic vision–language fusion into a CLIP-based segmentation pipeline.</em>
36
+ </p>
37
+
38
+ <br>
39
+
40
+ <p align="center">
41
+ <img src="assets/MedCLIPSeg_PVL.png" width="85%" />
42
+ <br>
43
+ <em>Schematic illustration of the proposed Probabilistic Vision–Language (PVL) adapters used for bidirectional cross-modal interaction.</em>
44
+ </p>
45
+
46
+
47
+ 1) **Bidirectional Vision–Language Fusion**: Introduce representation-level fusion modules that enable efficient bidirectional interaction between image and text features while keeping CLIP encoders frozen, improving data efficiency and robustness.
48
+
49
+ 2) **Probabilistic Cross-Modal Attention**: Model vision–language attention using variational Key–Value formulations to capture uncertainty, leading to improved segmentation accuracy and cross-domain generalization.
50
+
51
+ 3) **Pixel-Level Uncertainty Estimation**: Generate dense uncertainty maps by sampling attention Values from learned probability distributions, providing intuitive reliability estimates for clinical interpretation.
52
+
53
+ 4) **Extensive Multi-Modal Segmentation Evaluation**: Conduct comprehensive evaluation against state-of-the-art methods across 5 imaging modalities and 6 organs and 16 datasets, assessing data efficiency, domain generalization, and the contribution of individual model components.
54
+
55
+ ## Results
56
+ Results reported below show DSC scores (%) for data efficiency and domain generalization evaluation benchmarks across 16 biomedical image segmentation datasets averaged.
57
+
58
+ ### Data-Efficiency Evaluation
59
+
60
+ | **Method** | **10% Data** | **25% Data** | **50% Data** | **100% Data** |
61
+ |-----------|:------------:|:------------:|:------------:|:-------------:|
62
+ | [UNet](https://arxiv.org/abs/1505.04597) | 60.95 | 62.74 | 71.61 | 78.49 |
63
+ | [UNet++](https://arxiv.org/abs/1807.10165) | 63.72 | 65.86 | 73.15 | 78.44 |
64
+ | [DeepLabv3](https://arxiv.org/abs/1706.05587) | 61.32 | 65.39 | 68.58 | 73.28 |
65
+ | [Attention U-Net](https://arxiv.org/abs/1804.03999) | 62.78 | 64.97 | 71.34 | 76.30 |
66
+ | [nnU-Net](https://arxiv.org/abs/1809.10486) | 73.45 | 76.73 | 78.86 | 81.40 |
67
+ | [Swin-UNet](https://arxiv.org/abs/2105.05537) | 53.04 | 54.69 | 55.89 | 65.03 |
68
+ | [TransUNet](https://arxiv.org/abs/2102.04306) | 52.69 | 55.25 | 55.22 | 67.22 |
69
+ | [LViT](https://arxiv.org/abs/2206.14718) | 66.51 | 75.66 | 78.88 | 83.35 |
70
+ | [Ariadne’s Thread](https://arxiv.org/abs/2307.03942) | 61.34 | 63.09 | 65.65 | 70.07 |
71
+ | [EoMT-CLIP](https://arxiv.org/abs/2503.19108) | 74.07 | 76.29 | 79.19 | 82.93 |
72
+ | [CLIPSeg](https://arxiv.org/abs/2112.10003) | 74.66 | 78.31 | 79.63 | 84.87 |
73
+ | [DenseCLIP](https://arxiv.org/abs/2112.01518) | 67.84 | 70.23 | 72.09 | 74.19 |
74
+ | [ZegCLIP](https://arxiv.org/abs/2212.03588) | 61.25 | 72.46 | 76.21 | 78.98 |
75
+ | [SAN](https://arxiv.org/abs/2302.12242) | 74.13 | 76.13 | 78.80 | 81.62 |
76
+ | [MaPLe](https://arxiv.org/abs/2210.03117) | 66.27 | 71.53 | 74.60 | 74.60 |
77
+ | [MaPLe + Decoder](https://arxiv.org/abs/2210.03117) | 74.81 | 79.64 | 82.81 | 84.94 |
78
+ | [VLSM-Adapter](https://arxiv.org/abs/2405.06196) | 74.47 | 77.63 | 80.83 | 83.85 |
79
+ | [CausalCLIPSeg](https://arxiv.org/abs/2503.15949) | 71.19 | 75.42 | 78.60 | 81.34 |
80
+ | [CAT-Seg](https://arxiv.org/abs/2303.11797) | *78.76* | *81.12* | *83.32* | *85.90* |
81
+ | **[MedCLIPSeg (Ours)](https://arxiv.org/abs/XXXX.XXXXX)** | **81.10** | **85.08** | **87.18** | **88.66** |
82
+
83
+ ### Domain Generalization
84
+
85
+ | **Method** | **ID** | **OOD** | **HM** |
86
+ |-----------|:--------------------:|:---------------------:|:-----------------:|
87
+ | [LViT](https://arxiv.org/abs/2206.14718) | 83.31 | 64.99 | 73.02 |
88
+ | [Ariadne’s Thread](https://arxiv.org/abs/2307.03942) | 68.25 | 27.23 | 38.93 |
89
+ | [CLIPSeg](https://arxiv.org/abs/2112.10003) | 84.95 | 69.22 | 76.28 |
90
+ | [DenseCLIP](https://arxiv.org/abs/2112.01518) | 77.69 | 58.11 | 66.49 |
91
+ | [ZegCLIP](https://arxiv.org/abs/2212.03588) | 77.16 | 61.33 | 68.34 |
92
+ | [SAN](https://arxiv.org/abs/2302.12242) | 84.45 | 69.87 | 76.47 |
93
+ | [MaPLe](https://arxiv.org/abs/2210.03117) | 76.55 | 59.30 | 66.83 |
94
+ | [MaPLe + Decoder](https://arxiv.org/abs/2210.03117) | 84.78 | 66.85 | 74.76 |
95
+ | [VLSM-Adapter](https://arxiv.org/abs/2405.06196) | 85.78 | 73.28 | 79.04 |
96
+ | [CausalCLIPSeg](https://arxiv.org/abs/2503.15949) | 81.52 | 53.86 | 64.86 |
97
+ | [CAT-Seg](https://arxiv.org/abs/2303.11797) | 86.10 | 74.57 | 79.92 |
98
+ | **[MedCLIPSeg (Ours)](https://arxiv.org/abs/XXXX.XXXXX)** | **89.11** | **79.02** | **83.76** |
99
+
100
+ ### Segmentation and Uncertainty Visualization
101
+
102
+ <p align="center">
103
+ <img src="assets/MedCLIPSeg_Seg.png" width="100%" />
104
+ </p>
105
+
106
+ <p align="center">
107
+ <em>
108
+ Uncertainty peaks along lesion boundaries and remains consistent across diverse datasets, indicating reliable calibration and generalization.
109
+ In-distribution (ID) data are shown in <b style="color:#0000FF;">blue</b>, while out-of-distribution (OOD) data are shown in <b style="color:#FF0000;">red</b>.
110
+ </em>
111
+ </p>
112
+
113
+ ## Model Checkpoints
114
+ All the checkpoints can be found on the official [Hugging Face repo](https://huggingface.co/TahaKoleilat/MedCLIPSeg) for the Data Efficiency and Domain Generalization evaluation benchmarks. Take a look [here](https://github.com/HealthX-Lab/MedCLIPSeg/blob/main/assets/RUN.md#2-running-evaluation-from-given-checkpoints) to see how to run and reproduce all the results.
115
+
116
+ ## Installation
117
+ For installation and other package requirements, please follow the instructions detailed in [INSTALL.md](assets/INSTALL.md).
118
+
119
+ ## Data preparation
120
+ Please follow the instructions at [DATASETS.md](assets/DATASETS.md) to prepare all datasets.
121
+
122
+ ## Training and Evaluation
123
+ Please refer to the [RUN.md](assets/RUN.md) for detailed instructions on training, evaluating and reproducing the results using our pre-trained models.
124
+
125
+ <hr />
126
+
127
+ ## Citation
128
+ If you use our work, please consider citing:
129
+ ```bibtex
130
+ @inproceedings{koleilat2026medclipseg,
131
+ author = {Koleilat, Taha and Asgariandehkordi, Hojat and Nejatimanzari, Omid and Barile, Berardino and Xiao, Yiming and Rivaz, Hassan},
132
+ title = {MedCLIPSeg: Probabilistic Vision-Language Adaptation for Data-Efficient and Generalizable Medical Image Segmentation},
133
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
134
+ month = {June},
135
+ year = {2026},
136
+ pages = {1406-1417}
137
+ }
138
+ ```
139
+
140
+ ## Acknowledgements
141
+ We are grateful to the authors of [CLIP](https://github.com/openai/CLIP), [MaPLe](https://github.com/muzairkhattak/multimodal-prompt-learning), and [LViT](https://github.com/HUANGLIZI/LViT) for making their code publicly available. If you use our model or code, we kindly request that you also consider citing these foundational works.
reference_medclipseg/biomedclip/biomedclip.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from typing import Optional
4
+ from open_clip.hf_model import ClsPooler
5
+
6
+ class BiomedCLIP(nn.Module):
7
+ def __init__(self, model, prompt_depth = None, prompt_length = None, output_hidden_states=False):
8
+ super(BiomedCLIP, self).__init__()
9
+ self.vision_model = model.visual
10
+ self.text_model = model.text
11
+ self.logit_scale = model.logit_scale
12
+ self.prompt_depth = prompt_depth
13
+ self.output_hidden_states = output_hidden_states
14
+ self.prompt_tokens = nn.Parameter(torch.empty(prompt_depth, prompt_length, 768))
15
+ self.text_dtype = self.text_model.transformer.dtype
16
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
17
+ nn.init.normal_(self.prompt_tokens, mean=0, std=0.02)
18
+ def encode_image(self, x):
19
+ trunk = self.vision_model.trunk
20
+ x = trunk.patch_embed(x)
21
+ x = trunk._pos_embed(x)
22
+ x = trunk.norm_pre(x)
23
+
24
+ hidden_states = []
25
+
26
+ for i, block in enumerate(trunk.blocks):
27
+ if(i < self.prompt_depth - 1):
28
+ x = block(x, self.prompt_tokens[i])
29
+ else:
30
+ x = block(x)
31
+
32
+ hidden_states.append(x)
33
+
34
+ x = trunk.norm(x)
35
+
36
+ # if trunk.global_pool:
37
+ # x = (
38
+ # x[:, trunk.num_prefix_tokens :].mean(dim=1)
39
+ # if trunk.global_pool == "avg"
40
+ # else x[:, 0]
41
+ # )
42
+
43
+ x = trunk.fc_norm(x)
44
+ x = trunk.head(x)
45
+
46
+ # Linear Projection: 768 -> 512
47
+ x = self.vision_model.head(x)
48
+
49
+ if self.output_hidden_states:
50
+ return x, hidden_states
51
+ else:
52
+ return x
53
+ def encode_text(self, x, attention_mask: Optional[torch.LongTensor] = None, output_hidden_states: bool = False,):
54
+
55
+ if attention_mask is None:
56
+ attention_mask = (x != self.text_model.config.pad_token_id).long()
57
+
58
+ inputs_embeds = self.text_model.transformer.embeddings.word_embeddings(x).type(self.text_dtype).to(self.device)
59
+
60
+ x = self.text_model.transformer.embeddings(
61
+ inputs_embeds=inputs_embeds
62
+ )
63
+
64
+ extended_attention_mask = attention_mask[:, None, None, :]
65
+ extended_attention_mask = extended_attention_mask.to(dtype=self.text_dtype) # fp16 compatibility
66
+ extended_attention_mask = (1.0 - extended_attention_mask) * torch.finfo(self.text_dtype).min
67
+
68
+ for i, layer in enumerate(self.text_model.transformer.encoder.layer):
69
+ if(i < self.prompt_depth - 1):
70
+ x = layer(x, attention_mask=extended_attention_mask, prompt_tokens=self.prompt_tokens[i])
71
+ else:
72
+ x = layer(x, attention_mask=extended_attention_mask)
73
+ x = x[0]
74
+
75
+ pooled_out = x[:, 0, :]
76
+ projected = self.text_model.proj(pooled_out)
77
+
78
+
79
+ return projected
80
+
reference_medclipseg/biomedclip/layers/__init__.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .activations import *
2
+ from .adaptive_avgmax_pool import \
3
+ adaptive_avgmax_pool2d, select_adaptive_pool2d, AdaptiveAvgMaxPool2d, SelectAdaptivePool2d
4
+ from .attention2d import MultiQueryAttention2d, Attention2d, MultiQueryAttentionV2
5
+ from .attention_pool import AttentionPoolLatent
6
+ from .attention_pool2d import AttentionPool2d, RotAttentionPool2d, RotaryEmbedding
7
+ from .blur_pool import BlurPool2d, create_aa
8
+ from .classifier import create_classifier, ClassifierHead, NormMlpClassifierHead, ClNormMlpClassifierHead
9
+ from .cond_conv2d import CondConv2d, get_condconv_initializer
10
+ from .config import is_exportable, is_scriptable, is_no_jit, use_fused_attn, \
11
+ set_exportable, set_scriptable, set_no_jit, set_layer_config, set_fused_attn, \
12
+ set_reentrant_ckpt, use_reentrant_ckpt
13
+ from .conv2d_same import Conv2dSame, conv2d_same
14
+ from .conv_bn_act import ConvNormAct, ConvNormActAa, ConvBnAct
15
+ from .create_act import create_act_layer, get_act_layer, get_act_fn
16
+ from .create_attn import get_attn, create_attn
17
+ from .create_conv2d import create_conv2d
18
+ from .create_norm import get_norm_layer, create_norm_layer
19
+ from .create_norm_act import get_norm_act_layer, create_norm_act_layer, get_norm_act_layer
20
+ from .drop import DropBlock2d, DropPath, drop_block_2d, drop_path
21
+ from .eca import EcaModule, CecaModule, EfficientChannelAttn, CircularEfficientChannelAttn
22
+ from .evo_norm import EvoNorm2dB0, EvoNorm2dB1, EvoNorm2dB2,\
23
+ EvoNorm2dS0, EvoNorm2dS0a, EvoNorm2dS1, EvoNorm2dS1a, EvoNorm2dS2, EvoNorm2dS2a
24
+ from .fast_norm import is_fast_norm, set_fast_norm, fast_group_norm, fast_layer_norm
25
+ from .filter_response_norm import FilterResponseNormTlu2d, FilterResponseNormAct2d
26
+ from .format import Format, get_channel_dim, get_spatial_dim, nchw_to, nhwc_to
27
+ from .gather_excite import GatherExcite
28
+ from .global_context import GlobalContext
29
+ from .grid import ndgrid, meshgrid
30
+ from .helpers import to_ntuple, to_2tuple, to_3tuple, to_4tuple, make_divisible, extend_tuple
31
+ from .hybrid_embed import HybridEmbed, HybridEmbedWithSize
32
+ from .inplace_abn import InplaceAbn
33
+ from .layer_scale import LayerScale, LayerScale2d
34
+ from .linear import Linear
35
+ from .mixed_conv2d import MixedConv2d
36
+ from .mlp import Mlp, GluMlp, GatedMlp, SwiGLU, SwiGLUPacked, ConvMlp, GlobalResponseNormMlp
37
+ from .non_local_attn import NonLocalAttn, BatNonLocalAttn
38
+ from .norm import GroupNorm, GroupNorm1, LayerNorm, LayerNorm2d, RmsNorm, RmsNorm2d, SimpleNorm, SimpleNorm2d
39
+ from .norm_act import BatchNormAct2d, GroupNormAct, GroupNorm1Act, LayerNormAct, LayerNormAct2d,\
40
+ SyncBatchNormAct, convert_sync_batchnorm, FrozenBatchNormAct2d, freeze_batch_norm_2d, unfreeze_batch_norm_2d
41
+ from .padding import get_padding, get_same_padding, pad_same
42
+ from .patch_dropout import PatchDropout
43
+ from .patch_embed import PatchEmbed, PatchEmbedWithSize, resample_patch_embed
44
+ from .pool2d_same import AvgPool2dSame, create_pool2d
45
+ from .pos_embed import resample_abs_pos_embed, resample_abs_pos_embed_nhwc
46
+ from .pos_embed_rel import RelPosMlp, RelPosBias, RelPosBiasTf, gen_relative_position_index, gen_relative_log_coords, \
47
+ resize_rel_pos_bias_table, resize_rel_pos_bias_table_simple, resize_rel_pos_bias_table_levit
48
+ from .pos_embed_sincos import pixel_freq_bands, freq_bands, build_sincos2d_pos_embed, build_fourier_pos_embed, \
49
+ build_rotary_pos_embed, apply_rot_embed, apply_rot_embed_cat, apply_rot_embed_list, apply_keep_indices_nlc, \
50
+ FourierEmbed, RotaryEmbedding, RotaryEmbeddingCat
51
+ from .squeeze_excite import SEModule, SqueezeExcite, EffectiveSEModule, EffectiveSqueezeExcite
52
+ from .selective_kernel import SelectiveKernel
53
+ from .separable_conv import SeparableConv2d, SeparableConvNormAct
54
+ from .space_to_depth import SpaceToDepth, DepthToSpace
55
+ from .split_attn import SplitAttn
56
+ from .split_batchnorm import SplitBatchNorm2d, convert_splitbn_model
57
+ from .std_conv import StdConv2d, StdConv2dSame, ScaledStdConv2d, ScaledStdConv2dSame
58
+ from .test_time_pool import TestTimePoolHead, apply_test_time_pool
59
+ from .trace_utils import _assert, _float_to_int
60
+ from .typing import LayerType, PadType
61
+ from .weight_init import trunc_normal_, trunc_normal_tf_, variance_scaling_, lecun_normal_, \
62
+ init_weight_jax, init_weight_vit
reference_medclipseg/biomedclip/layers/activations.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Activations
2
+
3
+ A collection of activations fn and modules with a common interface so that they can
4
+ easily be swapped. All have an `inplace` arg even if not used.
5
+
6
+ Hacked together by / Copyright 2020 Ross Wightman
7
+ """
8
+
9
+ import torch
10
+ from torch import nn as nn
11
+ from torch.nn import functional as F
12
+
13
+
14
+ def swish(x, inplace: bool = False):
15
+ """Swish - Described in: https://arxiv.org/abs/1710.05941
16
+ """
17
+ return x.mul_(x.sigmoid()) if inplace else x.mul(x.sigmoid())
18
+
19
+
20
+ class Swish(nn.Module):
21
+ def __init__(self, inplace: bool = False):
22
+ super(Swish, self).__init__()
23
+ self.inplace = inplace
24
+
25
+ def forward(self, x):
26
+ return swish(x, self.inplace)
27
+
28
+
29
+ def mish(x, inplace: bool = False):
30
+ """Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681
31
+ NOTE: I don't have a working inplace variant
32
+ """
33
+ return x.mul(F.softplus(x).tanh())
34
+
35
+
36
+ class Mish(nn.Module):
37
+ """Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681
38
+ """
39
+ def __init__(self, inplace: bool = False):
40
+ super(Mish, self).__init__()
41
+
42
+ def forward(self, x):
43
+ return mish(x)
44
+
45
+
46
+ def sigmoid(x, inplace: bool = False):
47
+ return x.sigmoid_() if inplace else x.sigmoid()
48
+
49
+
50
+ # PyTorch has this, but not with a consistent inplace argument interface
51
+ class Sigmoid(nn.Module):
52
+ def __init__(self, inplace: bool = False):
53
+ super(Sigmoid, self).__init__()
54
+ self.inplace = inplace
55
+
56
+ def forward(self, x):
57
+ return x.sigmoid_() if self.inplace else x.sigmoid()
58
+
59
+
60
+ def tanh(x, inplace: bool = False):
61
+ return x.tanh_() if inplace else x.tanh()
62
+
63
+
64
+ # PyTorch has this, but not with a consistent inplace argument interface
65
+ class Tanh(nn.Module):
66
+ def __init__(self, inplace: bool = False):
67
+ super(Tanh, self).__init__()
68
+ self.inplace = inplace
69
+
70
+ def forward(self, x):
71
+ return x.tanh_() if self.inplace else x.tanh()
72
+
73
+
74
+ def hard_swish(x, inplace: bool = False):
75
+ inner = F.relu6(x + 3.).div_(6.)
76
+ return x.mul_(inner) if inplace else x.mul(inner)
77
+
78
+
79
+ class HardSwish(nn.Module):
80
+ def __init__(self, inplace: bool = False):
81
+ super(HardSwish, self).__init__()
82
+ self.inplace = inplace
83
+
84
+ def forward(self, x):
85
+ return hard_swish(x, self.inplace)
86
+
87
+
88
+ def hard_sigmoid(x, inplace: bool = False):
89
+ if inplace:
90
+ return x.add_(3.).clamp_(0., 6.).div_(6.)
91
+ else:
92
+ return F.relu6(x + 3.) / 6.
93
+
94
+
95
+ class HardSigmoid(nn.Module):
96
+ def __init__(self, inplace: bool = False):
97
+ super(HardSigmoid, self).__init__()
98
+ self.inplace = inplace
99
+
100
+ def forward(self, x):
101
+ return hard_sigmoid(x, self.inplace)
102
+
103
+
104
+ def hard_mish(x, inplace: bool = False):
105
+ """ Hard Mish
106
+ Experimental, based on notes by Mish author Diganta Misra at
107
+ https://github.com/digantamisra98/H-Mish/blob/0da20d4bc58e696b6803f2523c58d3c8a82782d0/README.md
108
+ """
109
+ if inplace:
110
+ return x.mul_(0.5 * (x + 2).clamp(min=0, max=2))
111
+ else:
112
+ return 0.5 * x * (x + 2).clamp(min=0, max=2)
113
+
114
+
115
+ class HardMish(nn.Module):
116
+ def __init__(self, inplace: bool = False):
117
+ super(HardMish, self).__init__()
118
+ self.inplace = inplace
119
+
120
+ def forward(self, x):
121
+ return hard_mish(x, self.inplace)
122
+
123
+
124
+ class PReLU(nn.PReLU):
125
+ """Applies PReLU (w/ dummy inplace arg)
126
+ """
127
+ def __init__(self, num_parameters: int = 1, init: float = 0.25, inplace: bool = False) -> None:
128
+ super(PReLU, self).__init__(num_parameters=num_parameters, init=init)
129
+
130
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
131
+ return F.prelu(input, self.weight)
132
+
133
+
134
+ def gelu(x: torch.Tensor, inplace: bool = False) -> torch.Tensor:
135
+ return F.gelu(x)
136
+
137
+
138
+ class GELU(nn.Module):
139
+ """Applies the Gaussian Error Linear Units function (w/ dummy inplace arg)
140
+ """
141
+ def __init__(self, inplace: bool = False):
142
+ super(GELU, self).__init__()
143
+
144
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
145
+ return F.gelu(input)
146
+
147
+
148
+ def gelu_tanh(x: torch.Tensor, inplace: bool = False) -> torch.Tensor:
149
+ return F.gelu(x, approximate='tanh')
150
+
151
+
152
+ class GELUTanh(nn.Module):
153
+ """Applies the Gaussian Error Linear Units function (w/ dummy inplace arg)
154
+ """
155
+ def __init__(self, inplace: bool = False):
156
+ super(GELUTanh, self).__init__()
157
+
158
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
159
+ return F.gelu(input, approximate='tanh')
160
+
161
+
162
+ def quick_gelu(x: torch.Tensor, inplace: bool = False) -> torch.Tensor:
163
+ return x * torch.sigmoid(1.702 * x)
164
+
165
+
166
+ class QuickGELU(nn.Module):
167
+ """Applies the Gaussian Error Linear Units function (w/ dummy inplace arg)
168
+ """
169
+ def __init__(self, inplace: bool = False):
170
+ super(QuickGELU, self).__init__()
171
+
172
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
173
+ return quick_gelu(input)
reference_medclipseg/biomedclip/layers/activations_me.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Activations (memory-efficient w/ custom autograd)
2
+
3
+ A collection of activations fn and modules with a common interface so that they can
4
+ easily be swapped. All have an `inplace` arg even if not used.
5
+
6
+ These activations are not compatible with jit scripting or ONNX export of the model, please use
7
+ basic versions of the activations.
8
+
9
+ Hacked together by / Copyright 2020 Ross Wightman
10
+ """
11
+
12
+ import torch
13
+ from torch import nn as nn
14
+ from torch.nn import functional as F
15
+
16
+
17
+ def swish_fwd(x):
18
+ return x.mul(torch.sigmoid(x))
19
+
20
+
21
+ def swish_bwd(x, grad_output):
22
+ x_sigmoid = torch.sigmoid(x)
23
+ return grad_output * (x_sigmoid * (1 + x * (1 - x_sigmoid)))
24
+
25
+
26
+ class SwishAutoFn(torch.autograd.Function):
27
+ """ optimised Swish w/ memory-efficient checkpoint
28
+ Inspired by conversation btw Jeremy Howard & Adam Pazske
29
+ https://twitter.com/jeremyphoward/status/1188251041835315200
30
+ """
31
+ @staticmethod
32
+ def symbolic(g, x):
33
+ return g.op("Mul", x, g.op("Sigmoid", x))
34
+
35
+ @staticmethod
36
+ def forward(ctx, x):
37
+ ctx.save_for_backward(x)
38
+ return swish_fwd(x)
39
+
40
+ @staticmethod
41
+ def backward(ctx, grad_output):
42
+ x = ctx.saved_tensors[0]
43
+ return swish_bwd(x, grad_output)
44
+
45
+
46
+ def swish_me(x, inplace=False):
47
+ return SwishAutoFn.apply(x)
48
+
49
+
50
+ class SwishMe(nn.Module):
51
+ def __init__(self, inplace: bool = False):
52
+ super(SwishMe, self).__init__()
53
+
54
+ def forward(self, x):
55
+ return SwishAutoFn.apply(x)
56
+
57
+
58
+ def mish_fwd(x):
59
+ return x.mul(torch.tanh(F.softplus(x)))
60
+
61
+
62
+ def mish_bwd(x, grad_output):
63
+ x_sigmoid = torch.sigmoid(x)
64
+ x_tanh_sp = F.softplus(x).tanh()
65
+ return grad_output.mul(x_tanh_sp + x * x_sigmoid * (1 - x_tanh_sp * x_tanh_sp))
66
+
67
+
68
+ class MishAutoFn(torch.autograd.Function):
69
+ """ Mish: A Self Regularized Non-Monotonic Neural Activation Function - https://arxiv.org/abs/1908.08681
70
+ A memory efficient variant of Mish
71
+ """
72
+ @staticmethod
73
+ def forward(ctx, x):
74
+ ctx.save_for_backward(x)
75
+ return mish_fwd(x)
76
+
77
+ @staticmethod
78
+ def backward(ctx, grad_output):
79
+ x = ctx.saved_tensors[0]
80
+ return mish_bwd(x, grad_output)
81
+
82
+
83
+ def mish_me(x, inplace=False):
84
+ return MishAutoFn.apply(x)
85
+
86
+
87
+ class MishMe(nn.Module):
88
+ def __init__(self, inplace: bool = False):
89
+ super(MishMe, self).__init__()
90
+
91
+ def forward(self, x):
92
+ return MishAutoFn.apply(x)
93
+
94
+
95
+ def hard_sigmoid_fwd(x, inplace: bool = False):
96
+ return (x + 3).clamp(min=0, max=6).div(6.)
97
+
98
+
99
+ def hard_sigmoid_bwd(x, grad_output):
100
+ m = torch.ones_like(x) * ((x >= -3.) & (x <= 3.)) / 6.
101
+ return grad_output * m
102
+
103
+
104
+ class HardSigmoidAutoFn(torch.autograd.Function):
105
+ @staticmethod
106
+ def forward(ctx, x):
107
+ ctx.save_for_backward(x)
108
+ return hard_sigmoid_fwd(x)
109
+
110
+ @staticmethod
111
+ def backward(ctx, grad_output):
112
+ x = ctx.saved_tensors[0]
113
+ return hard_sigmoid_bwd(x, grad_output)
114
+
115
+
116
+ def hard_sigmoid_me(x, inplace: bool = False):
117
+ return HardSigmoidAutoFn.apply(x)
118
+
119
+
120
+ class HardSigmoidMe(nn.Module):
121
+ def __init__(self, inplace: bool = False):
122
+ super(HardSigmoidMe, self).__init__()
123
+
124
+ def forward(self, x):
125
+ return HardSigmoidAutoFn.apply(x)
126
+
127
+
128
+ def hard_swish_fwd(x):
129
+ return x * (x + 3).clamp(min=0, max=6).div(6.)
130
+
131
+
132
+ def hard_swish_bwd(x, grad_output):
133
+ m = torch.ones_like(x) * (x >= 3.)
134
+ m = torch.where((x >= -3.) & (x <= 3.), x / 3. + .5, m)
135
+ return grad_output * m
136
+
137
+
138
+ class HardSwishAutoFn(torch.autograd.Function):
139
+ """A memory efficient HardSwish activation"""
140
+ @staticmethod
141
+ def forward(ctx, x):
142
+ ctx.save_for_backward(x)
143
+ return hard_swish_fwd(x)
144
+
145
+ @staticmethod
146
+ def backward(ctx, grad_output):
147
+ x = ctx.saved_tensors[0]
148
+ return hard_swish_bwd(x, grad_output)
149
+
150
+ @staticmethod
151
+ def symbolic(g, self):
152
+ input = g.op("Add", self, g.op('Constant', value_t=torch.tensor(3, dtype=torch.float)))
153
+ hardtanh_ = g.op("Clip", input, g.op('Constant', value_t=torch.tensor(0, dtype=torch.float)), g.op('Constant', value_t=torch.tensor(6, dtype=torch.float)))
154
+ hardtanh_ = g.op("Div", hardtanh_, g.op('Constant', value_t=torch.tensor(6, dtype=torch.float)))
155
+ return g.op("Mul", self, hardtanh_)
156
+
157
+
158
+ def hard_swish_me(x, inplace=False):
159
+ return HardSwishAutoFn.apply(x)
160
+
161
+
162
+ class HardSwishMe(nn.Module):
163
+ def __init__(self, inplace: bool = False):
164
+ super(HardSwishMe, self).__init__()
165
+
166
+ def forward(self, x):
167
+ return HardSwishAutoFn.apply(x)
168
+
169
+
170
+ def hard_mish_fwd(x):
171
+ return 0.5 * x * (x + 2).clamp(min=0, max=2)
172
+
173
+
174
+ def hard_mish_bwd(x, grad_output):
175
+ m = torch.ones_like(x) * (x >= -2.)
176
+ m = torch.where((x >= -2.) & (x <= 0.), x + 1., m)
177
+ return grad_output * m
178
+
179
+
180
+ class HardMishAutoFn(torch.autograd.Function):
181
+ """ A memory efficient variant of Hard Mish
182
+ Experimental, based on notes by Mish author Diganta Misra at
183
+ https://github.com/digantamisra98/H-Mish/blob/0da20d4bc58e696b6803f2523c58d3c8a82782d0/README.md
184
+ """
185
+ @staticmethod
186
+ def forward(ctx, x):
187
+ ctx.save_for_backward(x)
188
+ return hard_mish_fwd(x)
189
+
190
+ @staticmethod
191
+ def backward(ctx, grad_output):
192
+ x = ctx.saved_tensors[0]
193
+ return hard_mish_bwd(x, grad_output)
194
+
195
+
196
+ def hard_mish_me(x, inplace: bool = False):
197
+ return HardMishAutoFn.apply(x)
198
+
199
+
200
+ class HardMishMe(nn.Module):
201
+ def __init__(self, inplace: bool = False):
202
+ super(HardMishMe, self).__init__()
203
+
204
+ def forward(self, x):
205
+ return HardMishAutoFn.apply(x)
206
+
207
+
208
+
reference_medclipseg/biomedclip/layers/adaptive_avgmax_pool.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ PyTorch selectable adaptive pooling
2
+ Adaptive pooling with the ability to select the type of pooling from:
3
+ * 'avg' - Average pooling
4
+ * 'max' - Max pooling
5
+ * 'avgmax' - Sum of average and max pooling re-scaled by 0.5
6
+ * 'avgmaxc' - Concatenation of average and max pooling along feature dim, doubles feature dim
7
+
8
+ Both a functional and a nn.Module version of the pooling is provided.
9
+
10
+ Hacked together by / Copyright 2020 Ross Wightman
11
+ """
12
+ from typing import Optional, Tuple, Union
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+
18
+ from .format import get_spatial_dim, get_channel_dim
19
+
20
+ _int_tuple_2_t = Union[int, Tuple[int, int]]
21
+
22
+
23
+ def adaptive_pool_feat_mult(pool_type='avg'):
24
+ if pool_type.endswith('catavgmax'):
25
+ return 2
26
+ else:
27
+ return 1
28
+
29
+
30
+ def adaptive_avgmax_pool2d(x, output_size: _int_tuple_2_t = 1):
31
+ x_avg = F.adaptive_avg_pool2d(x, output_size)
32
+ x_max = F.adaptive_max_pool2d(x, output_size)
33
+ return 0.5 * (x_avg + x_max)
34
+
35
+
36
+ def adaptive_catavgmax_pool2d(x, output_size: _int_tuple_2_t = 1):
37
+ x_avg = F.adaptive_avg_pool2d(x, output_size)
38
+ x_max = F.adaptive_max_pool2d(x, output_size)
39
+ return torch.cat((x_avg, x_max), 1)
40
+
41
+
42
+ def select_adaptive_pool2d(x, pool_type='avg', output_size: _int_tuple_2_t = 1):
43
+ """Selectable global pooling function with dynamic input kernel size
44
+ """
45
+ if pool_type == 'avg':
46
+ x = F.adaptive_avg_pool2d(x, output_size)
47
+ elif pool_type == 'avgmax':
48
+ x = adaptive_avgmax_pool2d(x, output_size)
49
+ elif pool_type == 'catavgmax':
50
+ x = adaptive_catavgmax_pool2d(x, output_size)
51
+ elif pool_type == 'max':
52
+ x = F.adaptive_max_pool2d(x, output_size)
53
+ else:
54
+ assert False, 'Invalid pool type: %s' % pool_type
55
+ return x
56
+
57
+
58
+ class FastAdaptiveAvgPool(nn.Module):
59
+ def __init__(self, flatten: bool = False, input_fmt: F = 'NCHW'):
60
+ super(FastAdaptiveAvgPool, self).__init__()
61
+ self.flatten = flatten
62
+ self.dim = get_spatial_dim(input_fmt)
63
+
64
+ def forward(self, x):
65
+ return x.mean(self.dim, keepdim=not self.flatten)
66
+
67
+
68
+ class FastAdaptiveMaxPool(nn.Module):
69
+ def __init__(self, flatten: bool = False, input_fmt: str = 'NCHW'):
70
+ super(FastAdaptiveMaxPool, self).__init__()
71
+ self.flatten = flatten
72
+ self.dim = get_spatial_dim(input_fmt)
73
+
74
+ def forward(self, x):
75
+ return x.amax(self.dim, keepdim=not self.flatten)
76
+
77
+
78
+ class FastAdaptiveAvgMaxPool(nn.Module):
79
+ def __init__(self, flatten: bool = False, input_fmt: str = 'NCHW'):
80
+ super(FastAdaptiveAvgMaxPool, self).__init__()
81
+ self.flatten = flatten
82
+ self.dim = get_spatial_dim(input_fmt)
83
+
84
+ def forward(self, x):
85
+ x_avg = x.mean(self.dim, keepdim=not self.flatten)
86
+ x_max = x.amax(self.dim, keepdim=not self.flatten)
87
+ return 0.5 * x_avg + 0.5 * x_max
88
+
89
+
90
+ class FastAdaptiveCatAvgMaxPool(nn.Module):
91
+ def __init__(self, flatten: bool = False, input_fmt: str = 'NCHW'):
92
+ super(FastAdaptiveCatAvgMaxPool, self).__init__()
93
+ self.flatten = flatten
94
+ self.dim_reduce = get_spatial_dim(input_fmt)
95
+ if flatten:
96
+ self.dim_cat = 1
97
+ else:
98
+ self.dim_cat = get_channel_dim(input_fmt)
99
+
100
+ def forward(self, x):
101
+ x_avg = x.mean(self.dim_reduce, keepdim=not self.flatten)
102
+ x_max = x.amax(self.dim_reduce, keepdim=not self.flatten)
103
+ return torch.cat((x_avg, x_max), self.dim_cat)
104
+
105
+
106
+ class AdaptiveAvgMaxPool2d(nn.Module):
107
+ def __init__(self, output_size: _int_tuple_2_t = 1):
108
+ super(AdaptiveAvgMaxPool2d, self).__init__()
109
+ self.output_size = output_size
110
+
111
+ def forward(self, x):
112
+ return adaptive_avgmax_pool2d(x, self.output_size)
113
+
114
+
115
+ class AdaptiveCatAvgMaxPool2d(nn.Module):
116
+ def __init__(self, output_size: _int_tuple_2_t = 1):
117
+ super(AdaptiveCatAvgMaxPool2d, self).__init__()
118
+ self.output_size = output_size
119
+
120
+ def forward(self, x):
121
+ return adaptive_catavgmax_pool2d(x, self.output_size)
122
+
123
+
124
+ class SelectAdaptivePool2d(nn.Module):
125
+ """Selectable global pooling layer with dynamic input kernel size
126
+ """
127
+ def __init__(
128
+ self,
129
+ output_size: _int_tuple_2_t = 1,
130
+ pool_type: str = 'fast',
131
+ flatten: bool = False,
132
+ input_fmt: str = 'NCHW',
133
+ ):
134
+ super(SelectAdaptivePool2d, self).__init__()
135
+ assert input_fmt in ('NCHW', 'NHWC')
136
+ self.pool_type = pool_type or '' # convert other falsy values to empty string for consistent TS typing
137
+ pool_type = pool_type.lower()
138
+ if not pool_type:
139
+ self.pool = nn.Identity() # pass through
140
+ self.flatten = nn.Flatten(1) if flatten else nn.Identity()
141
+ elif pool_type.startswith('fast') or input_fmt != 'NCHW':
142
+ assert output_size == 1, 'Fast pooling and non NCHW input formats require output_size == 1.'
143
+ if pool_type.endswith('catavgmax'):
144
+ self.pool = FastAdaptiveCatAvgMaxPool(flatten, input_fmt=input_fmt)
145
+ elif pool_type.endswith('avgmax'):
146
+ self.pool = FastAdaptiveAvgMaxPool(flatten, input_fmt=input_fmt)
147
+ elif pool_type.endswith('max'):
148
+ self.pool = FastAdaptiveMaxPool(flatten, input_fmt=input_fmt)
149
+ elif pool_type == 'fast' or pool_type.endswith('avg'):
150
+ self.pool = FastAdaptiveAvgPool(flatten, input_fmt=input_fmt)
151
+ else:
152
+ assert False, 'Invalid pool type: %s' % pool_type
153
+ self.flatten = nn.Identity()
154
+ else:
155
+ assert input_fmt == 'NCHW'
156
+ if pool_type == 'avgmax':
157
+ self.pool = AdaptiveAvgMaxPool2d(output_size)
158
+ elif pool_type == 'catavgmax':
159
+ self.pool = AdaptiveCatAvgMaxPool2d(output_size)
160
+ elif pool_type == 'max':
161
+ self.pool = nn.AdaptiveMaxPool2d(output_size)
162
+ elif pool_type == 'avg':
163
+ self.pool = nn.AdaptiveAvgPool2d(output_size)
164
+ else:
165
+ assert False, 'Invalid pool type: %s' % pool_type
166
+ self.flatten = nn.Flatten(1) if flatten else nn.Identity()
167
+
168
+ def is_identity(self):
169
+ return not self.pool_type
170
+
171
+ def forward(self, x):
172
+ x = self.pool(x)
173
+ x = self.flatten(x)
174
+ return x
175
+
176
+ def feat_mult(self):
177
+ return adaptive_pool_feat_mult(self.pool_type)
178
+
179
+ def __repr__(self):
180
+ return self.__class__.__name__ + '(' \
181
+ + 'pool_type=' + self.pool_type \
182
+ + ', flatten=' + str(self.flatten) + ')'
183
+
reference_medclipseg/biomedclip/layers/attention2d.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Type, Union
2
+
3
+ import torch
4
+ from torch import nn as nn
5
+ from torch.nn import functional as F
6
+
7
+ from .config import use_fused_attn
8
+ from .create_conv2d import create_conv2d
9
+ from .helpers import to_2tuple
10
+ from .pool2d_same import create_pool2d
11
+
12
+
13
+ class MultiQueryAttentionV2(nn.Module):
14
+ """Multi Query Attention.
15
+
16
+ Fast Transformer Decoding: One Write-Head is All You Need
17
+ https://arxiv.org/pdf/1911.02150.pdf
18
+
19
+ This is an acceletor optimized version - removing multiple unnecessary
20
+ tensor transpose by re-arranging indices according to the following rules: 1)
21
+ contracted indices are at the end, 2) other indices have the same order in the
22
+ input and output tensores.
23
+
24
+ Compared to V1, this gives 3x speed up.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ dim: int,
30
+ dim_out: Optional[int] = None,
31
+ num_heads: int = 8,
32
+ key_dim: int = 64,
33
+ value_dim: int = 64,
34
+ attn_drop: float = 0.,
35
+ proj_drop: float = 0.,
36
+ ):
37
+ """Initializer."""
38
+ super().__init__()
39
+ dim_out = dim_out or dim
40
+ self.num_heads = num_heads
41
+ self.key_dim = key_dim
42
+ self.value_dim = value_dim
43
+ self.scale = key_dim ** -0.5
44
+
45
+ self.query_proj = nn.Parameter(torch.randn([self.num_heads, self.key_dim, dim]))
46
+ self.key_proj = nn.Parameter(torch.randn([dim, self.key_dim]))
47
+ self.value_proj = nn.Parameter(torch.randn([dim, self.value_dim]))
48
+ self.attn_drop = nn.Dropout(attn_drop)
49
+ self.out_proj = nn.Parameter(torch.randn([dim_out, self.num_heads, self.value_dim]))
50
+ self.proj_drop = nn.Dropout(proj_drop)
51
+
52
+ def _reshape_input(self, t):
53
+ """Reshapes a tensor to three dimensions, keeping the first and last."""
54
+ s = t.shape
55
+ # Propagate the shape statically where possible.
56
+ #num = t.shape[1:-1].numel()
57
+ #return t.reshape(s[0], num, s[-1])
58
+ return t.reshape(s[0], s[1], -1).transpose(1, 2)
59
+
60
+ def forward(self, x, m: Optional[torch.Tensor] = None):
61
+ """Run layer computation."""
62
+ b, _, h, w = x.shape
63
+ m = m if m is not None else x
64
+
65
+ reshaped_x = self._reshape_input(x)
66
+ reshaped_m = self._reshape_input(m)
67
+
68
+ q = torch.einsum('bnd,hkd->bnhk', reshaped_x, self.query_proj)
69
+ k = torch.einsum('bmd,dk->bmk', reshaped_m, self.key_proj)
70
+
71
+ attn = torch.einsum('bnhk,bmk->bnhm', q, k) * self.scale
72
+ attn = attn.softmax(dim=-1)
73
+ attn = self.attn_drop(attn)
74
+
75
+ v = torch.einsum('bmd,dv->bmv', reshaped_m, self.value_proj)
76
+ o = torch.einsum('bnhm,bmv->bnhv', attn, v)
77
+ result = torch.einsum('bnhv,dhv->bdn', o, self.out_proj)
78
+ result = self.proj_drop(result)
79
+ return result.reshape(b, -1, h, w)
80
+
81
+
82
+ class MultiQueryAttention2d(nn.Module):
83
+ """Multi Query Attention with spatial downsampling.
84
+
85
+ 3 parameters are introduced for the spatial downsampling:
86
+ 1. kv_stride: downsampling factor on Key and Values only.
87
+ 2. query_strides: horizontal & vertical strides on Query only.
88
+
89
+ This is an optimized version.
90
+ 1. Projections in Attention is explicit written out as 1x1 Conv2D.
91
+ 2. Additional reshapes are introduced to bring a up to 3x speed up.
92
+ """
93
+ fused_attn: torch.jit.Final[bool]
94
+
95
+ def __init__(
96
+ self,
97
+ dim: int,
98
+ dim_out: Optional[int] = None,
99
+ num_heads: int = 8,
100
+ key_dim: Optional[int] = None,
101
+ value_dim: Optional[int] = None,
102
+ query_strides: int = 1,
103
+ kv_stride: int = 1,
104
+ dw_kernel_size: int = 3,
105
+ dilation: int = 1,
106
+ padding: Union[str, int, List[int]] = '',
107
+ attn_drop: float = 0.,
108
+ proj_drop: float = 0.,
109
+ norm_layer: Type[nn.Module] = nn.BatchNorm2d,
110
+ use_bias: bool = False,
111
+ ):
112
+ """Initializer.
113
+
114
+ Args:
115
+ num_heads: Number of attention heads.
116
+ key_dim: Size of the attention key dimension.
117
+ value_dim: Size of the attention value dimension.
118
+ query_strides: Vertical stride size for query only.
119
+ kv_stride: Key and value stride size.
120
+ dw_kernel_size: Spatial dimension of the depthwise kernel.
121
+ """
122
+ super().__init__()
123
+ dim_out = dim_out or dim
124
+ self.num_heads = num_heads
125
+ self.key_dim = key_dim or dim // num_heads
126
+ self.value_dim = value_dim or dim // num_heads
127
+ self.query_strides = to_2tuple(query_strides)
128
+ self.kv_stride = kv_stride
129
+ self.has_query_strides = any([s > 1 for s in self.query_strides])
130
+ self.scale = self.key_dim ** -0.5
131
+ self.fused_attn = use_fused_attn()
132
+ self.drop = attn_drop
133
+
134
+ self.query = nn.Sequential()
135
+ if self.has_query_strides:
136
+ # FIXME dilation
137
+ if padding == 'same':
138
+ self.query.add_module('down_pool', create_pool2d(
139
+ 'avg',
140
+ kernel_size=self.query_strides,
141
+ padding='same',
142
+ ))
143
+ else:
144
+ # no pad if not 'same' as kern=stride=even
145
+ self.query.add_module('down_pool', nn.AvgPool2d(kernel_size=query_strides))
146
+ self.query.add_module('norm', norm_layer(dim))
147
+ self.query.add_module('proj', create_conv2d(
148
+ dim,
149
+ self.num_heads * self.key_dim,
150
+ kernel_size=1,
151
+ bias=use_bias,
152
+ ))
153
+
154
+ self.key = nn.Sequential()
155
+ if kv_stride > 1:
156
+ self.key.add_module('down_conv', create_conv2d(
157
+ dim,
158
+ dim,
159
+ kernel_size=dw_kernel_size,
160
+ stride=kv_stride,
161
+ dilation=dilation,
162
+ padding=padding,
163
+ depthwise=True,
164
+ ))
165
+ self.key.add_module('norm', norm_layer(dim))
166
+ self.key.add_module('proj', create_conv2d(
167
+ dim,
168
+ self.key_dim,
169
+ kernel_size=1,
170
+ padding=padding,
171
+ bias=use_bias,
172
+ ))
173
+
174
+ self.value = nn.Sequential()
175
+ if kv_stride > 1:
176
+ self.value.add_module('down_conv', create_conv2d(
177
+ dim,
178
+ dim,
179
+ kernel_size=dw_kernel_size,
180
+ stride=kv_stride,
181
+ dilation=dilation,
182
+ padding=padding,
183
+ depthwise=True,
184
+ ))
185
+ self.value.add_module('norm', norm_layer(dim))
186
+ self.value.add_module('proj', create_conv2d(
187
+ dim,
188
+ self.value_dim,
189
+ kernel_size=1,
190
+ bias=use_bias,
191
+ ))
192
+
193
+ self.attn_drop = nn.Dropout(attn_drop)
194
+
195
+ self.output = nn.Sequential()
196
+ if self.has_query_strides:
197
+ self.output.add_module('upsample', nn.Upsample(scale_factor=self.query_strides, mode='bilinear', align_corners=False))
198
+ self.output.add_module('proj', create_conv2d(
199
+ self.value_dim * self.num_heads,
200
+ dim_out,
201
+ kernel_size=1,
202
+ bias=use_bias,
203
+ ))
204
+ self.output.add_module('drop', nn.Dropout(proj_drop))
205
+
206
+ self.einsum = False
207
+
208
+ def init_weights(self):
209
+ # using xavier appeared to improve stability for mobilenetv4 hybrid w/ this layer
210
+ nn.init.xavier_uniform_(self.query.proj.weight)
211
+ nn.init.xavier_uniform_(self.key.proj.weight)
212
+ nn.init.xavier_uniform_(self.value.proj.weight)
213
+ if self.kv_stride > 1:
214
+ nn.init.xavier_uniform_(self.key.down_conv.weight)
215
+ nn.init.xavier_uniform_(self.value.down_conv.weight)
216
+ nn.init.xavier_uniform_(self.output.proj.weight)
217
+
218
+ def _reshape_input(self, t: torch.Tensor):
219
+ """Reshapes a tensor to three dimensions, keeping the batch and channels."""
220
+ s = t.shape
221
+ t = t.reshape(s[0], s[1], -1).transpose(1, 2)
222
+ if self.einsum:
223
+ return t
224
+ else:
225
+ return t.unsqueeze(1).contiguous()
226
+
227
+ def _reshape_projected_query(self, t: torch.Tensor, num_heads: int, key_dim: int):
228
+ """Reshapes projected query: [b, n, n, h x k] -> [b, n x n, h, k]."""
229
+ s = t.shape
230
+ t = t.reshape(s[0], num_heads, key_dim, -1)
231
+ if self.einsum:
232
+ return t.permute(0, 3, 1, 2).contiguous()
233
+ else:
234
+ return t.transpose(-1, -2).contiguous()
235
+
236
+ def _reshape_output(self, t: torch.Tensor, num_heads: int, h_px: int, w_px: int):
237
+ """Reshape output:[b, n x n x h, k] -> [b, n, n, hk]."""
238
+ s = t.shape
239
+ feat_dim = s[-1] * num_heads
240
+ if not self.einsum:
241
+ t = t.transpose(1, 2)
242
+ return t.reshape(s[0], h_px, w_px, feat_dim).permute(0, 3, 1, 2).contiguous()
243
+
244
+ def forward(self, x, attn_mask: Optional[torch.Tensor] = None):
245
+ """Run layer computation."""
246
+ B, C, H, W = s = x.shape
247
+
248
+ q = self.query(x)
249
+ # desired q shape: [b, h, k, n x n] - [b, l, h, k]
250
+ q = self._reshape_projected_query(q, self.num_heads, self.key_dim)
251
+
252
+ k = self.key(x)
253
+ # output shape of k: [b, k, p], p = m x m
254
+ k = self._reshape_input(k)
255
+
256
+ v = self.value(x)
257
+ # output shape of v: [ b, p, k], p = m x m
258
+ v = self._reshape_input(v)
259
+
260
+ # desired q shape: [b, n x n, h, k]
261
+ # desired k shape: [b, m x m, k]
262
+ # desired logits shape: [b, n x n, h, m x m]
263
+ if self.einsum:
264
+ attn = torch.einsum('blhk,bpk->blhp', q, k) * self.scale
265
+ if attn_mask is not None:
266
+ # NOTE: assumes mask is float and in correct shape
267
+ attn = attn + attn_mask
268
+ attn = attn.softmax(dim=-1)
269
+ attn = self.attn_drop(attn)
270
+ o = torch.einsum('blhp,bpk->blhk', attn, v)
271
+ else:
272
+ if self.fused_attn:
273
+ o = F.scaled_dot_product_attention(
274
+ q, k, v,
275
+ attn_mask=attn_mask,
276
+ dropout_p=self.attn_drop.p if self.training else 0.
277
+ )
278
+ else:
279
+ q = q * self.scale
280
+ attn = q @ k.transpose(-1, -2)
281
+ if attn_mask is not None:
282
+ # NOTE: assumes mask is float and in correct shape
283
+ attn = attn + attn_mask
284
+ attn = attn.softmax(dim=-1)
285
+ attn = self.attn_drop(attn)
286
+ o = attn @ v
287
+
288
+ # reshape o into [b, hk, n, n,]
289
+ o = self._reshape_output(o, self.num_heads, H // self.query_strides[0], W // self.query_strides[1])
290
+ x = self.output(o)
291
+ return x
292
+
293
+
294
+ class Attention2d(nn.Module):
295
+ fused_attn: torch.jit.Final[bool]
296
+
297
+ """ multi-head attention for 2D NCHW tensors"""
298
+ def __init__(
299
+ self,
300
+ dim: int,
301
+ dim_out: Optional[int] = None,
302
+ num_heads: int = 32,
303
+ bias: bool = True,
304
+ expand_first: bool = False,
305
+ head_first: bool = False,
306
+ attn_drop: float = 0.,
307
+ proj_drop: float = 0.
308
+ ):
309
+ super().__init__()
310
+ dim_out = dim_out or dim
311
+ dim_attn = dim_out if expand_first else dim
312
+ self.num_heads = num_heads
313
+ self.dim_head = dim_attn // num_heads
314
+ self.head_first = head_first
315
+ self.fused_attn = use_fused_attn()
316
+
317
+ self.qkv = nn.Conv2d(dim, dim_attn * 3, 1, bias=bias)
318
+ self.attn_drop = nn.Dropout(attn_drop)
319
+ self.proj = nn.Conv2d(dim_attn, dim_out, 1, bias=bias)
320
+ self.proj_drop = nn.Dropout(proj_drop)
321
+
322
+ def forward(self, x, attn_mask: Optional[torch.Tensor] = None):
323
+ B, C, H, W = x.shape
324
+
325
+ if self.head_first:
326
+ q, k, v = self.qkv(x).view(B, self.num_heads, self.dim_head * 3, -1).chunk(3, dim=2)
327
+ else:
328
+ q, k, v = self.qkv(x).reshape(B, 3, self.num_heads, self.dim_head, -1).unbind(1)
329
+
330
+ if self.fused_attn:
331
+ x = torch.nn.functional.scaled_dot_product_attention(
332
+ q.transpose(-1, -2).contiguous(),
333
+ k.transpose(-1, -2).contiguous(),
334
+ v.transpose(-1, -2).contiguous(),
335
+ attn_mask=attn_mask,
336
+ dropout_p=self.attn_drop.p if self.training else 0.,
337
+ ).transpose(-1, -2).reshape(B, -1, H, W)
338
+ else:
339
+ q = q.transpose(-1, -2)
340
+ v = v.transpose(-1, -2)
341
+ attn = q @ k * q.size(-1) ** -0.5
342
+ if attn_mask is not None:
343
+ # NOTE: assumes mask is float and in correct shape
344
+ attn = attn + attn_mask
345
+ attn = attn.softmax(dim=-1)
346
+ attn = self.attn_drop(attn)
347
+ x = (attn @ v).transpose(-1, -2).reshape(B, -1, H, W)
348
+
349
+ x = self.proj(x)
350
+ x = self.proj_drop(x)
351
+ return x
reference_medclipseg/biomedclip/layers/attention_pool.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+
7
+ from .config import use_fused_attn
8
+ from .mlp import Mlp
9
+ from .weight_init import trunc_normal_tf_
10
+
11
+
12
+ class AttentionPoolLatent(nn.Module):
13
+ """ Attention pooling w/ latent query
14
+ """
15
+ fused_attn: torch.jit.Final[bool]
16
+
17
+ def __init__(
18
+ self,
19
+ in_features: int,
20
+ out_features: int = None,
21
+ embed_dim: int = None,
22
+ num_heads: int = 8,
23
+ feat_size: Optional[int] = None,
24
+ mlp_ratio: float = 4.0,
25
+ qkv_bias: bool = True,
26
+ qk_norm: bool = False,
27
+ latent_len: int = 1,
28
+ latent_dim: int = None,
29
+ pos_embed: str = '',
30
+ pool_type: str = 'token',
31
+ norm_layer: Optional[nn.Module] = None,
32
+ drop: float = 0.0,
33
+ ):
34
+ super().__init__()
35
+ embed_dim = embed_dim or in_features
36
+ out_features = out_features or in_features
37
+ assert embed_dim % num_heads == 0
38
+ self.num_heads = num_heads
39
+ self.head_dim = embed_dim // num_heads
40
+ self.feat_size = feat_size
41
+ self.scale = self.head_dim ** -0.5
42
+ self.pool = pool_type
43
+ self.fused_attn = use_fused_attn()
44
+
45
+ if pos_embed == 'abs':
46
+ assert feat_size is not None
47
+ self.pos_embed = nn.Parameter(torch.zeros(feat_size, in_features))
48
+ else:
49
+ self.pos_embed = None
50
+
51
+ self.latent_dim = latent_dim or embed_dim
52
+ self.latent_len = latent_len
53
+ self.latent = nn.Parameter(torch.zeros(1, self.latent_len, embed_dim))
54
+
55
+ self.q = nn.Linear(embed_dim, embed_dim, bias=qkv_bias)
56
+ self.kv = nn.Linear(embed_dim, embed_dim * 2, bias=qkv_bias)
57
+ self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
58
+ self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
59
+ self.proj = nn.Linear(embed_dim, embed_dim)
60
+ self.proj_drop = nn.Dropout(drop)
61
+
62
+ self.norm = norm_layer(out_features) if norm_layer is not None else nn.Identity()
63
+ self.mlp = Mlp(embed_dim, int(embed_dim * mlp_ratio))
64
+
65
+ self.init_weights()
66
+
67
+ def init_weights(self):
68
+ if self.pos_embed is not None:
69
+ trunc_normal_tf_(self.pos_embed, std=self.pos_embed.shape[1] ** -0.5)
70
+ trunc_normal_tf_(self.latent, std=self.latent_dim ** -0.5)
71
+
72
+ def forward(self, x):
73
+ B, N, C = x.shape
74
+
75
+ if self.pos_embed is not None:
76
+ # FIXME interpolate
77
+ x = x + self.pos_embed.unsqueeze(0).to(x.dtype)
78
+
79
+ q_latent = self.latent.expand(B, -1, -1)
80
+ q = self.q(q_latent).reshape(B, self.latent_len, self.num_heads, self.head_dim).transpose(1, 2)
81
+
82
+ kv = self.kv(x).reshape(B, N, 2, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
83
+ k, v = kv.unbind(0)
84
+
85
+ q, k = self.q_norm(q), self.k_norm(k)
86
+
87
+ if self.fused_attn:
88
+ x = F.scaled_dot_product_attention(q, k, v)
89
+ else:
90
+ q = q * self.scale
91
+ attn = q @ k.transpose(-2, -1)
92
+ attn = attn.softmax(dim=-1)
93
+ x = attn @ v
94
+ x = x.transpose(1, 2).reshape(B, self.latent_len, C)
95
+ x = self.proj(x)
96
+ x = self.proj_drop(x)
97
+
98
+ x = x + self.mlp(self.norm(x))
99
+
100
+ # optional pool if latent seq_len > 1 and pooled output is desired
101
+ if self.pool == 'token':
102
+ x = x[:, 0]
103
+ elif self.pool == 'avg':
104
+ x = x.mean(1)
105
+ return x
reference_medclipseg/biomedclip/layers/attention_pool2d.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Attention Pool 2D
2
+
3
+ Implementations of 2D spatial feature pooling using multi-head attention instead of average pool.
4
+
5
+ Based on idea in CLIP by OpenAI, licensed Apache 2.0
6
+ https://github.com/openai/CLIP/blob/3b473b0e682c091a9e53623eebc1ca1657385717/clip/model.py
7
+
8
+ Hacked together by / Copyright 2021 Ross Wightman
9
+ """
10
+ from typing import Optional, Union, Tuple
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+
15
+ from. config import use_fused_attn
16
+ from .helpers import to_2tuple
17
+ from .pos_embed import resample_abs_pos_embed
18
+ from .pos_embed_sincos import apply_rot_embed, RotaryEmbedding
19
+ from .weight_init import trunc_normal_
20
+
21
+
22
+ class RotAttentionPool2d(nn.Module):
23
+ """ Attention based 2D feature pooling w/ rotary (relative) pos embedding.
24
+ This is a multi-head attention based replacement for (spatial) average pooling in NN architectures.
25
+
26
+ Adapted from the AttentionPool2d in CLIP w/ rotary embedding instead of learned embed.
27
+ https://github.com/openai/CLIP/blob/3b473b0e682c091a9e53623eebc1ca1657385717/clip/model.py
28
+
29
+ NOTE: While this impl does not require a fixed feature size, performance at differeing resolutions from
30
+ train varies widely and falls off dramatically. I'm not sure if there is a way around this... -RW
31
+ """
32
+ fused_attn: torch.jit.Final[bool]
33
+
34
+ def __init__(
35
+ self,
36
+ in_features: int,
37
+ out_features: Optional[int] = None,
38
+ ref_feat_size: Union[int, Tuple[int, int]] = 7,
39
+ embed_dim: Optional[int] = None,
40
+ head_dim: Optional[int] = 64,
41
+ num_heads: Optional[int] = None,
42
+ qkv_bias: bool = True,
43
+ qkv_separate: bool = False,
44
+ pool_type: str = 'token',
45
+ class_token: bool = False,
46
+ drop_rate: float = 0.,
47
+ ):
48
+ super().__init__()
49
+ assert pool_type in ('', 'token')
50
+ self.embed_dim = embed_dim = embed_dim or in_features
51
+ self.in_features = in_features
52
+ self.out_features = out_features or in_features
53
+ ref_feat_size = to_2tuple(ref_feat_size)
54
+ if num_heads is not None:
55
+ assert embed_dim % num_heads == 0
56
+ head_dim = embed_dim // num_heads
57
+ else:
58
+ assert embed_dim % head_dim == 0
59
+ num_heads = embed_dim // head_dim
60
+ self.num_heads = num_heads
61
+ self.head_dim = head_dim
62
+ self.pool_type = pool_type.lower()
63
+ self.scale = self.head_dim ** -0.5
64
+ self.fused_attn = use_fused_attn()
65
+
66
+ if class_token:
67
+ self.cls_token = nn.Parameter(torch.zeros(1, embed_dim))
68
+ else:
69
+ self.cls_token = None
70
+
71
+ if qkv_separate:
72
+ self.q = nn.Linear(in_features, embed_dim, bias=qkv_bias)
73
+ self.k = nn.Linear(in_features, embed_dim, bias=qkv_bias)
74
+ self.v = nn.Linear(in_features, embed_dim, bias=qkv_bias)
75
+ self.qkv = None
76
+ else:
77
+ self.qkv = nn.Linear(in_features, embed_dim * 3, bias=qkv_bias)
78
+ self.drop = nn.Dropout(drop_rate)
79
+ self.proj = nn.Linear(embed_dim, self.out_features)
80
+ self.pos_embed = RotaryEmbedding(self.head_dim, in_pixels=False, ref_feat_shape=ref_feat_size)
81
+
82
+ def init_weights(self, zero_init_last: bool = False):
83
+ if self.qkv is None:
84
+ in_features = self.q.in_features
85
+ trunc_normal_(self.q.weight, std=in_features ** -0.5)
86
+ nn.init.zeros_(self.q.bias)
87
+ trunc_normal_(self.k.weight, std=in_features ** -0.5)
88
+ nn.init.zeros_(self.k.bias)
89
+ trunc_normal_(self.v.weight, std=in_features ** -0.5)
90
+ nn.init.zeros_(self.v.bias)
91
+ else:
92
+ in_features = self.qkv.in_features
93
+ trunc_normal_(self.qkv.weight, std=in_features ** -0.5)
94
+ nn.init.zeros_(self.qkv.bias)
95
+
96
+ def reset(self, num_classes: Optional[int] = None, pool_type: Optional[str] = None):
97
+ # NOTE: this module is being used as a head, so need compatible reset()
98
+ if pool_type is not None:
99
+ assert pool_type in ('', 'token')
100
+ self.pool_type = pool_type
101
+ if num_classes is not None:
102
+ self.proj = nn.Linear(self.in_features, num_classes) if num_classes > 0 else nn.Identity()
103
+ self.out_features = num_classes if num_classes > 0 else self.embed_dim
104
+
105
+ def _pool(self, x: torch.Tensor, H: int, W: int) -> torch.Tensor:
106
+ if self.pool_type == 'token':
107
+ x = x[:, 0]
108
+ else:
109
+ # if not pooled, return spatial output without token
110
+ x = x[:, 1:].reshape(x.shape[0], H, W, -1).permute(0, 3, 1, 2)
111
+ return x
112
+
113
+ def forward(self, x, pre_logits: bool = False):
114
+ B, _, H, W = x.shape
115
+ N = H * W
116
+ x = x.flatten(2).transpose(1, 2)
117
+ if self.cls_token is None:
118
+ x = torch.cat([x.mean(1, keepdim=True), x], dim=1)
119
+ else:
120
+ x = torch.cat([self.cls_token.expand(x.shape[0], -1, -1), x], dim=1)
121
+ if self.qkv is None:
122
+ q = self.q(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
123
+ k = self.k(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
124
+ v = self.v(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
125
+ else:
126
+ x = self.qkv(x).reshape(B, N + 1, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
127
+ q, k, v = x.unbind(0)
128
+
129
+ rse, rce = self.pos_embed.get_embed((H, W))
130
+ q = torch.cat([q[:, :, :1, :], apply_rot_embed(q[:, :, 1:, :], rse, rce)], dim=2).type_as(v)
131
+ k = torch.cat([k[:, :, :1, :], apply_rot_embed(k[:, :, 1:, :], rse, rce)], dim=2).type_as(v)
132
+
133
+ if self.fused_attn:
134
+ x = nn.functional.scaled_dot_product_attention(q, k, v)
135
+ else:
136
+ q = q * self.scale
137
+ attn = q @ k.transpose(-2, -1)
138
+ attn = attn.softmax(dim=-1)
139
+ x = attn @ v
140
+ x = x.transpose(1, 2).reshape(B, N + 1, -1)
141
+ x = self.drop(x)
142
+ if pre_logits:
143
+ x = self._pool(x, H, W)
144
+ return x
145
+ x = self.proj(x)
146
+ x = self._pool(x, H, W)
147
+ return x
148
+
149
+
150
+ class AttentionPool2d(nn.Module):
151
+ """ Attention based 2D feature pooling w/ learned (absolute) pos embedding.
152
+ This is a multi-head attention based replacement for (spatial) average pooling in NN architectures.
153
+
154
+ It was based on impl in CLIP by OpenAI
155
+ https://github.com/openai/CLIP/blob/3b473b0e682c091a9e53623eebc1ca1657385717/clip/model.py
156
+
157
+ NOTE: This requires feature size upon construction and well prevent adaptive sizing of the network.
158
+ """
159
+ fused_attn: torch.jit.Final[bool]
160
+
161
+ def __init__(
162
+ self,
163
+ in_features: int,
164
+ feat_size: Union[int, Tuple[int, int]] = 7,
165
+ out_features: Optional[int] = None,
166
+ embed_dim: Optional[int] = None,
167
+ head_dim: Optional[int] = 64,
168
+ num_heads: Optional[int] = None,
169
+ qkv_bias: bool = True,
170
+ qkv_separate: bool = False,
171
+ pool_type: str = 'token',
172
+ class_token: bool = False,
173
+ drop_rate: float = 0.,
174
+ ):
175
+ super().__init__()
176
+ assert pool_type in ('', 'token')
177
+ self.embed_dim = embed_dim = embed_dim or in_features
178
+ self.in_features = in_features
179
+ self.out_features = out_features or in_features
180
+ if num_heads is not None:
181
+ assert embed_dim % num_heads == 0
182
+ head_dim = embed_dim // num_heads
183
+ else:
184
+ assert embed_dim % head_dim == 0
185
+ num_heads = embed_dim // head_dim
186
+ self.feat_size = to_2tuple(feat_size)
187
+ self.seq_len = self.feat_size[0] * self.feat_size[1]
188
+ self.num_heads = num_heads
189
+ self.head_dim = head_dim
190
+ self.pool_type = pool_type
191
+ self.scale = self.head_dim ** -0.5
192
+ self.fused_attn = use_fused_attn()
193
+
194
+ if class_token:
195
+ self.cls_token = nn.Parameter(torch.zeros(1, embed_dim))
196
+ else:
197
+ self.cls_token = None
198
+
199
+ if qkv_separate:
200
+ self.q = nn.Linear(in_features, embed_dim, bias=qkv_bias)
201
+ self.k = nn.Linear(in_features, embed_dim, bias=qkv_bias)
202
+ self.v = nn.Linear(in_features, embed_dim, bias=qkv_bias)
203
+ self.qkv = None
204
+ else:
205
+ self.q = self.k = self.v = None
206
+ self.qkv = nn.Linear(in_features, embed_dim * 3, bias=qkv_bias)
207
+ self.drop = nn.Dropout(drop_rate)
208
+ self.proj = nn.Linear(embed_dim, self.out_features)
209
+ self.pos_embed = nn.Parameter(torch.zeros(self.seq_len + 1, in_features))
210
+
211
+ self.init_weights()
212
+
213
+ def init_weights(self, zero_init_last: bool = False):
214
+ if self.qkv is None:
215
+ in_features = self.q.in_features
216
+ trunc_normal_(self.q.weight, std=in_features ** -0.5)
217
+ nn.init.zeros_(self.q.bias)
218
+ trunc_normal_(self.k.weight, std=in_features ** -0.5)
219
+ nn.init.zeros_(self.k.bias)
220
+ trunc_normal_(self.v.weight, std=in_features ** -0.5)
221
+ nn.init.zeros_(self.v.bias)
222
+ else:
223
+ in_features = self.qkv.in_features
224
+ trunc_normal_(self.qkv.weight, std=in_features ** -0.5)
225
+ nn.init.zeros_(self.qkv.bias)
226
+ trunc_normal_(self.pos_embed, std=in_features ** -0.5)
227
+
228
+ def reset(self, num_classes: Optional[int] = None, pool_type: Optional[str] = None):
229
+ # NOTE: this module is being used as a head, so need compatible reset()
230
+ if pool_type is not None:
231
+ assert pool_type in ('', 'token')
232
+ self.pool_type = pool_type
233
+ if num_classes is not None:
234
+ self.proj = nn.Linear(self.in_features, num_classes) if num_classes > 0 else nn.Identity()
235
+ self.out_features = num_classes if num_classes > 0 else self.embed_dim
236
+
237
+ def _pool(self, x: torch.Tensor, H: int, W: int) -> torch.Tensor:
238
+ if self.pool_type == 'token':
239
+ x = x[:, 0]
240
+ else:
241
+ # if not pooled, return spatial output without token
242
+ x = x[:, 1:].reshape(x.shape[0], H, W, -1).permute(0, 3, 1, 2)
243
+ return x
244
+
245
+ def forward(self, x, pre_logits: bool = False):
246
+ B, _, H, W = x.shape
247
+ N = H * W
248
+ x = x.flatten(2).transpose(1, 2)
249
+ if self.cls_token is None:
250
+ x = torch.cat([x.mean(1, keepdim=True), x], dim=1)
251
+ else:
252
+ x = torch.cat([self.cls_token.expand(x.shape[0], -1, -1), x], dim=1)
253
+ pos_embed = resample_abs_pos_embed(self.pos_embed.unsqueeze(0), (H, W), num_prefix_tokens=1)
254
+ x = x + pos_embed
255
+
256
+ if self.qkv is None:
257
+ q = self.q(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
258
+ k = self.k(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
259
+ v = self.v(x).reshape(B, N + 1, self.num_heads, self.head_dim).transpose(1, 2)
260
+ else:
261
+ x = self.qkv(x).reshape(B, -1, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
262
+ q, k, v = x.unbind(0)
263
+
264
+ if self.fused_attn:
265
+ x = nn.functional.scaled_dot_product_attention(q, k, v)
266
+ else:
267
+ q = q * self.scale
268
+ attn = q @ k.transpose(-2, -1)
269
+ attn = attn.softmax(dim=-1)
270
+ x = attn @ v
271
+ x = x.transpose(1, 2).reshape(B, N + 1, -1)
272
+ x = self.drop(x)
273
+ if pre_logits:
274
+ x = self._pool(x, H, W)
275
+ return x
276
+ x = self.proj(x)
277
+ x = self._pool(x, H, W)
278
+ return x
reference_medclipseg/biomedclip/layers/blur_pool.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BlurPool layer inspired by
3
+ - Kornia's Max_BlurPool2d
4
+ - Making Convolutional Networks Shift-Invariant Again :cite:`zhang2019shiftinvar`
5
+
6
+ Hacked together by Chris Ha and Ross Wightman
7
+ """
8
+ from functools import partial
9
+ from typing import Optional, Type
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ import numpy as np
15
+
16
+ from .padding import get_padding
17
+ from .typing import LayerType
18
+
19
+
20
+ class BlurPool2d(nn.Module):
21
+ r"""Creates a module that computes blurs and downsample a given feature map.
22
+ See :cite:`zhang2019shiftinvar` for more details.
23
+ Corresponds to the Downsample class, which does blurring and subsampling
24
+
25
+ Args:
26
+ channels = Number of input channels
27
+ filt_size (int): binomial filter size for blurring. currently supports 3 (default) and 5.
28
+ stride (int): downsampling filter stride
29
+
30
+ Returns:
31
+ torch.Tensor: the transformed tensor.
32
+ """
33
+ def __init__(
34
+ self,
35
+ channels: Optional[int] = None,
36
+ filt_size: int = 3,
37
+ stride: int = 2,
38
+ pad_mode: str = 'reflect',
39
+ ) -> None:
40
+ super(BlurPool2d, self).__init__()
41
+ assert filt_size > 1
42
+ self.channels = channels
43
+ self.filt_size = filt_size
44
+ self.stride = stride
45
+ self.pad_mode = pad_mode
46
+ self.padding = [get_padding(filt_size, stride, dilation=1)] * 4
47
+
48
+ coeffs = torch.tensor((np.poly1d((0.5, 0.5)) ** (self.filt_size - 1)).coeffs.astype(np.float32))
49
+ blur_filter = (coeffs[:, None] * coeffs[None, :])[None, None, :, :]
50
+ if channels is not None:
51
+ blur_filter = blur_filter.repeat(self.channels, 1, 1, 1)
52
+ self.register_buffer('filt', blur_filter, persistent=False)
53
+
54
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
55
+ x = F.pad(x, self.padding, mode=self.pad_mode)
56
+ if self.channels is None:
57
+ channels = x.shape[1]
58
+ weight = self.filt.expand(channels, 1, self.filt_size, self.filt_size)
59
+ else:
60
+ channels = self.channels
61
+ weight = self.filt
62
+ return F.conv2d(x, weight, stride=self.stride, groups=channels)
63
+
64
+
65
+ def create_aa(
66
+ aa_layer: LayerType,
67
+ channels: Optional[int] = None,
68
+ stride: int = 2,
69
+ enable: bool = True,
70
+ noop: Optional[Type[nn.Module]] = nn.Identity
71
+ ) -> nn.Module:
72
+ """ Anti-aliasing """
73
+ if not aa_layer or not enable:
74
+ return noop() if noop is not None else None
75
+
76
+ if isinstance(aa_layer, str):
77
+ aa_layer = aa_layer.lower().replace('_', '').replace('-', '')
78
+ if aa_layer == 'avg' or aa_layer == 'avgpool':
79
+ aa_layer = nn.AvgPool2d
80
+ elif aa_layer == 'blur' or aa_layer == 'blurpool':
81
+ aa_layer = BlurPool2d
82
+ elif aa_layer == 'blurpc':
83
+ aa_layer = partial(BlurPool2d, pad_mode='constant')
84
+
85
+ else:
86
+ assert False, f"Unknown anti-aliasing layer ({aa_layer})."
87
+
88
+ try:
89
+ return aa_layer(channels=channels, stride=stride)
90
+ except TypeError as e:
91
+ return aa_layer(stride)
reference_medclipseg/biomedclip/layers/bottleneck_attn.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Bottleneck Self Attention (Bottleneck Transformers)
2
+
3
+ Paper: `Bottleneck Transformers for Visual Recognition` - https://arxiv.org/abs/2101.11605
4
+
5
+ @misc{2101.11605,
6
+ Author = {Aravind Srinivas and Tsung-Yi Lin and Niki Parmar and Jonathon Shlens and Pieter Abbeel and Ashish Vaswani},
7
+ Title = {Bottleneck Transformers for Visual Recognition},
8
+ Year = {2021},
9
+ }
10
+
11
+ Based on ref gist at: https://gist.github.com/aravindsrinivas/56359b79f0ce4449bcb04ab4b56a57a2
12
+
13
+ This impl is a WIP but given that it is based on the ref gist likely not too far off.
14
+
15
+ Hacked together by / Copyright 2021 Ross Wightman
16
+ """
17
+ from typing import List
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ from .helpers import to_2tuple, make_divisible
24
+ from .weight_init import trunc_normal_
25
+ from .trace_utils import _assert
26
+
27
+
28
+ def rel_logits_1d(q, rel_k, permute_mask: List[int]):
29
+ """ Compute relative logits along one dimension
30
+
31
+ As per: https://gist.github.com/aravindsrinivas/56359b79f0ce4449bcb04ab4b56a57a2
32
+ Originally from: `Attention Augmented Convolutional Networks` - https://arxiv.org/abs/1904.09925
33
+
34
+ Args:
35
+ q: (batch, heads, height, width, dim)
36
+ rel_k: (2 * width - 1, dim)
37
+ permute_mask: permute output dim according to this
38
+ """
39
+ B, H, W, dim = q.shape
40
+ x = (q @ rel_k.transpose(-1, -2))
41
+ x = x.reshape(-1, W, 2 * W -1)
42
+
43
+ # pad to shift from relative to absolute indexing
44
+ x_pad = F.pad(x, [0, 1]).flatten(1)
45
+ x_pad = F.pad(x_pad, [0, W - 1])
46
+
47
+ # reshape and slice out the padded elements
48
+ x_pad = x_pad.reshape(-1, W + 1, 2 * W - 1)
49
+ x = x_pad[:, :W, W - 1:]
50
+
51
+ # reshape and tile
52
+ x = x.reshape(B, H, 1, W, W).expand(-1, -1, H, -1, -1)
53
+ return x.permute(permute_mask)
54
+
55
+
56
+ class PosEmbedRel(nn.Module):
57
+ """ Relative Position Embedding
58
+ As per: https://gist.github.com/aravindsrinivas/56359b79f0ce4449bcb04ab4b56a57a2
59
+ Originally from: `Attention Augmented Convolutional Networks` - https://arxiv.org/abs/1904.09925
60
+ """
61
+ def __init__(self, feat_size, dim_head, scale):
62
+ super().__init__()
63
+ self.height, self.width = to_2tuple(feat_size)
64
+ self.dim_head = dim_head
65
+ self.height_rel = nn.Parameter(torch.randn(self.height * 2 - 1, dim_head) * scale)
66
+ self.width_rel = nn.Parameter(torch.randn(self.width * 2 - 1, dim_head) * scale)
67
+
68
+ def forward(self, q):
69
+ B, HW, _ = q.shape
70
+
71
+ # relative logits in width dimension.
72
+ q = q.reshape(B, self.height, self.width, -1)
73
+ rel_logits_w = rel_logits_1d(q, self.width_rel, permute_mask=(0, 1, 3, 2, 4))
74
+
75
+ # relative logits in height dimension.
76
+ q = q.transpose(1, 2)
77
+ rel_logits_h = rel_logits_1d(q, self.height_rel, permute_mask=(0, 3, 1, 4, 2))
78
+
79
+ rel_logits = rel_logits_h + rel_logits_w
80
+ rel_logits = rel_logits.reshape(B, HW, HW)
81
+ return rel_logits
82
+
83
+
84
+ class BottleneckAttn(nn.Module):
85
+ """ Bottleneck Attention
86
+ Paper: `Bottleneck Transformers for Visual Recognition` - https://arxiv.org/abs/2101.11605
87
+
88
+ The internal dimensions of the attention module are controlled by the interaction of several arguments.
89
+ * the output dimension of the module is specified by dim_out, which falls back to input dim if not set
90
+ * the value (v) dimension is set to dim_out // num_heads, the v projection determines the output dim
91
+ * the query and key (qk) dimensions are determined by
92
+ * num_heads * dim_head if dim_head is not None
93
+ * num_heads * (dim_out * attn_ratio // num_heads) if dim_head is None
94
+ * as seen above, attn_ratio determines the ratio of q and k relative to the output if dim_head not used
95
+
96
+ Args:
97
+ dim (int): input dimension to the module
98
+ dim_out (int): output dimension of the module, same as dim if not set
99
+ stride (int): output stride of the module, avg pool used if stride == 2 (default: 1).
100
+ num_heads (int): parallel attention heads (default: 4)
101
+ dim_head (int): dimension of query and key heads, calculated from dim_out * attn_ratio // num_heads if not set
102
+ qk_ratio (float): ratio of q and k dimensions to output dimension when dim_head not set. (default: 1.0)
103
+ qkv_bias (bool): add bias to q, k, and v projections
104
+ scale_pos_embed (bool): scale the position embedding as well as Q @ K
105
+ """
106
+ def __init__(
107
+ self, dim, dim_out=None, feat_size=None, stride=1, num_heads=4, dim_head=None,
108
+ qk_ratio=1.0, qkv_bias=False, scale_pos_embed=False):
109
+ super().__init__()
110
+ assert feat_size is not None, 'A concrete feature size matching expected input (H, W) is required'
111
+ dim_out = dim_out or dim
112
+ assert dim_out % num_heads == 0
113
+ self.num_heads = num_heads
114
+ self.dim_head_qk = dim_head or make_divisible(dim_out * qk_ratio, divisor=8) // num_heads
115
+ self.dim_head_v = dim_out // self.num_heads
116
+ self.dim_out_qk = num_heads * self.dim_head_qk
117
+ self.dim_out_v = num_heads * self.dim_head_v
118
+ self.scale = self.dim_head_qk ** -0.5
119
+ self.scale_pos_embed = scale_pos_embed
120
+
121
+ self.qkv = nn.Conv2d(dim, self.dim_out_qk * 2 + self.dim_out_v, 1, bias=qkv_bias)
122
+
123
+ # NOTE I'm only supporting relative pos embedding for now
124
+ self.pos_embed = PosEmbedRel(feat_size, dim_head=self.dim_head_qk, scale=self.scale)
125
+
126
+ self.pool = nn.AvgPool2d(2, 2) if stride == 2 else nn.Identity()
127
+
128
+ self.reset_parameters()
129
+
130
+ def reset_parameters(self):
131
+ trunc_normal_(self.qkv.weight, std=self.qkv.weight.shape[1] ** -0.5) # fan-in
132
+ trunc_normal_(self.pos_embed.height_rel, std=self.scale)
133
+ trunc_normal_(self.pos_embed.width_rel, std=self.scale)
134
+
135
+ def forward(self, x):
136
+ B, C, H, W = x.shape
137
+ _assert(H == self.pos_embed.height, '')
138
+ _assert(W == self.pos_embed.width, '')
139
+
140
+ x = self.qkv(x) # B, (2 * dim_head_qk + dim_head_v) * num_heads, H, W
141
+
142
+ # NOTE head vs channel split ordering in qkv projection was decided before I allowed qk to differ from v
143
+ # So, this is more verbose than if heads were before qkv splits, but throughput is not impacted.
144
+ q, k, v = torch.split(x, [self.dim_out_qk, self.dim_out_qk, self.dim_out_v], dim=1)
145
+ q = q.reshape(B * self.num_heads, self.dim_head_qk, -1).transpose(-1, -2)
146
+ k = k.reshape(B * self.num_heads, self.dim_head_qk, -1) # no transpose, for q @ k
147
+ v = v.reshape(B * self.num_heads, self.dim_head_v, -1).transpose(-1, -2)
148
+
149
+ if self.scale_pos_embed:
150
+ attn = (q @ k + self.pos_embed(q)) * self.scale # B * num_heads, H * W, H * W
151
+ else:
152
+ attn = (q @ k) * self.scale + self.pos_embed(q)
153
+ attn = attn.softmax(dim=-1)
154
+
155
+ out = (attn @ v).transpose(-1, -2).reshape(B, self.dim_out_v, H, W) # B, dim_out, H, W
156
+ out = self.pool(out)
157
+ return out
reference_medclipseg/biomedclip/layers/cbam.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ CBAM (sort-of) Attention
2
+
3
+ Experimental impl of CBAM: Convolutional Block Attention Module: https://arxiv.org/abs/1807.06521
4
+
5
+ WARNING: Results with these attention layers have been mixed. They can significantly reduce performance on
6
+ some tasks, especially fine-grained it seems. I may end up removing this impl.
7
+
8
+ Hacked together by / Copyright 2020 Ross Wightman
9
+ """
10
+ import torch
11
+ from torch import nn as nn
12
+ import torch.nn.functional as F
13
+
14
+ from .conv_bn_act import ConvNormAct
15
+ from .create_act import create_act_layer, get_act_layer
16
+ from .helpers import make_divisible
17
+
18
+
19
+ class ChannelAttn(nn.Module):
20
+ """ Original CBAM channel attention module, currently avg + max pool variant only.
21
+ """
22
+ def __init__(
23
+ self, channels, rd_ratio=1./16, rd_channels=None, rd_divisor=1,
24
+ act_layer=nn.ReLU, gate_layer='sigmoid', mlp_bias=False):
25
+ super(ChannelAttn, self).__init__()
26
+ if not rd_channels:
27
+ rd_channels = make_divisible(channels * rd_ratio, rd_divisor, round_limit=0.)
28
+ self.fc1 = nn.Conv2d(channels, rd_channels, 1, bias=mlp_bias)
29
+ self.act = act_layer(inplace=True)
30
+ self.fc2 = nn.Conv2d(rd_channels, channels, 1, bias=mlp_bias)
31
+ self.gate = create_act_layer(gate_layer)
32
+
33
+ def forward(self, x):
34
+ x_avg = self.fc2(self.act(self.fc1(x.mean((2, 3), keepdim=True))))
35
+ x_max = self.fc2(self.act(self.fc1(x.amax((2, 3), keepdim=True))))
36
+ return x * self.gate(x_avg + x_max)
37
+
38
+
39
+ class LightChannelAttn(ChannelAttn):
40
+ """An experimental 'lightweight' that sums avg + max pool first
41
+ """
42
+ def __init__(
43
+ self, channels, rd_ratio=1./16, rd_channels=None, rd_divisor=1,
44
+ act_layer=nn.ReLU, gate_layer='sigmoid', mlp_bias=False):
45
+ super(LightChannelAttn, self).__init__(
46
+ channels, rd_ratio, rd_channels, rd_divisor, act_layer, gate_layer, mlp_bias)
47
+
48
+ def forward(self, x):
49
+ x_pool = 0.5 * x.mean((2, 3), keepdim=True) + 0.5 * x.amax((2, 3), keepdim=True)
50
+ x_attn = self.fc2(self.act(self.fc1(x_pool)))
51
+ return x * F.sigmoid(x_attn)
52
+
53
+
54
+ class SpatialAttn(nn.Module):
55
+ """ Original CBAM spatial attention module
56
+ """
57
+ def __init__(self, kernel_size=7, gate_layer='sigmoid'):
58
+ super(SpatialAttn, self).__init__()
59
+ self.conv = ConvNormAct(2, 1, kernel_size, apply_act=False)
60
+ self.gate = create_act_layer(gate_layer)
61
+
62
+ def forward(self, x):
63
+ x_attn = torch.cat([x.mean(dim=1, keepdim=True), x.amax(dim=1, keepdim=True)], dim=1)
64
+ x_attn = self.conv(x_attn)
65
+ return x * self.gate(x_attn)
66
+
67
+
68
+ class LightSpatialAttn(nn.Module):
69
+ """An experimental 'lightweight' variant that sums avg_pool and max_pool results.
70
+ """
71
+ def __init__(self, kernel_size=7, gate_layer='sigmoid'):
72
+ super(LightSpatialAttn, self).__init__()
73
+ self.conv = ConvNormAct(1, 1, kernel_size, apply_act=False)
74
+ self.gate = create_act_layer(gate_layer)
75
+
76
+ def forward(self, x):
77
+ x_attn = 0.5 * x.mean(dim=1, keepdim=True) + 0.5 * x.amax(dim=1, keepdim=True)
78
+ x_attn = self.conv(x_attn)
79
+ return x * self.gate(x_attn)
80
+
81
+
82
+ class CbamModule(nn.Module):
83
+ def __init__(
84
+ self, channels, rd_ratio=1./16, rd_channels=None, rd_divisor=1,
85
+ spatial_kernel_size=7, act_layer=nn.ReLU, gate_layer='sigmoid', mlp_bias=False):
86
+ super(CbamModule, self).__init__()
87
+ self.channel = ChannelAttn(
88
+ channels, rd_ratio=rd_ratio, rd_channels=rd_channels,
89
+ rd_divisor=rd_divisor, act_layer=act_layer, gate_layer=gate_layer, mlp_bias=mlp_bias)
90
+ self.spatial = SpatialAttn(spatial_kernel_size, gate_layer=gate_layer)
91
+
92
+ def forward(self, x):
93
+ x = self.channel(x)
94
+ x = self.spatial(x)
95
+ return x
96
+
97
+
98
+ class LightCbamModule(nn.Module):
99
+ def __init__(
100
+ self, channels, rd_ratio=1./16, rd_channels=None, rd_divisor=1,
101
+ spatial_kernel_size=7, act_layer=nn.ReLU, gate_layer='sigmoid', mlp_bias=False):
102
+ super(LightCbamModule, self).__init__()
103
+ self.channel = LightChannelAttn(
104
+ channels, rd_ratio=rd_ratio, rd_channels=rd_channels,
105
+ rd_divisor=rd_divisor, act_layer=act_layer, gate_layer=gate_layer, mlp_bias=mlp_bias)
106
+ self.spatial = LightSpatialAttn(spatial_kernel_size)
107
+
108
+ def forward(self, x):
109
+ x = self.channel(x)
110
+ x = self.spatial(x)
111
+ return x
112
+
reference_medclipseg/biomedclip/layers/classifier.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Classifier head and layer factory
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+ from collections import OrderedDict
6
+ from functools import partial
7
+ from typing import Optional, Union, Callable
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ from torch.nn import functional as F
12
+
13
+ from .adaptive_avgmax_pool import SelectAdaptivePool2d
14
+ from .create_act import get_act_layer
15
+ from .create_norm import get_norm_layer
16
+
17
+
18
+ def _create_pool(
19
+ num_features: int,
20
+ num_classes: int,
21
+ pool_type: str = 'avg',
22
+ use_conv: bool = False,
23
+ input_fmt: Optional[str] = None,
24
+ ):
25
+ flatten_in_pool = not use_conv # flatten when we use a Linear layer after pooling
26
+ if not pool_type:
27
+ flatten_in_pool = False # disable flattening if pooling is pass-through (no pooling)
28
+ global_pool = SelectAdaptivePool2d(
29
+ pool_type=pool_type,
30
+ flatten=flatten_in_pool,
31
+ input_fmt=input_fmt,
32
+ )
33
+ num_pooled_features = num_features * global_pool.feat_mult()
34
+ return global_pool, num_pooled_features
35
+
36
+
37
+ def _create_fc(num_features, num_classes, use_conv=False):
38
+ if num_classes <= 0:
39
+ fc = nn.Identity() # pass-through (no classifier)
40
+ elif use_conv:
41
+ fc = nn.Conv2d(num_features, num_classes, 1, bias=True)
42
+ else:
43
+ fc = nn.Linear(num_features, num_classes, bias=True)
44
+ return fc
45
+
46
+
47
+ def create_classifier(
48
+ num_features: int,
49
+ num_classes: int,
50
+ pool_type: str = 'avg',
51
+ use_conv: bool = False,
52
+ input_fmt: str = 'NCHW',
53
+ drop_rate: Optional[float] = None,
54
+ ):
55
+ global_pool, num_pooled_features = _create_pool(
56
+ num_features,
57
+ num_classes,
58
+ pool_type,
59
+ use_conv=use_conv,
60
+ input_fmt=input_fmt,
61
+ )
62
+ fc = _create_fc(
63
+ num_pooled_features,
64
+ num_classes,
65
+ use_conv=use_conv,
66
+ )
67
+ if drop_rate is not None:
68
+ dropout = nn.Dropout(drop_rate)
69
+ return global_pool, dropout, fc
70
+ return global_pool, fc
71
+
72
+
73
+ class ClassifierHead(nn.Module):
74
+ """Classifier head w/ configurable global pooling and dropout."""
75
+
76
+ def __init__(
77
+ self,
78
+ in_features: int,
79
+ num_classes: int,
80
+ pool_type: str = 'avg',
81
+ drop_rate: float = 0.,
82
+ use_conv: bool = False,
83
+ input_fmt: str = 'NCHW',
84
+ ):
85
+ """
86
+ Args:
87
+ in_features: The number of input features.
88
+ num_classes: The number of classes for the final classifier layer (output).
89
+ pool_type: Global pooling type, pooling disabled if empty string ('').
90
+ drop_rate: Pre-classifier dropout rate.
91
+ """
92
+ super(ClassifierHead, self).__init__()
93
+ self.in_features = in_features
94
+ self.use_conv = use_conv
95
+ self.input_fmt = input_fmt
96
+
97
+ global_pool, fc = create_classifier(
98
+ in_features,
99
+ num_classes,
100
+ pool_type,
101
+ use_conv=use_conv,
102
+ input_fmt=input_fmt,
103
+ )
104
+ self.global_pool = global_pool
105
+ self.drop = nn.Dropout(drop_rate)
106
+ self.fc = fc
107
+ self.flatten = nn.Flatten(1) if use_conv and pool_type else nn.Identity()
108
+
109
+ def reset(self, num_classes: int, pool_type: Optional[str] = None):
110
+ if pool_type is not None and pool_type != self.global_pool.pool_type:
111
+ self.global_pool, self.fc = create_classifier(
112
+ self.in_features,
113
+ num_classes,
114
+ pool_type=pool_type,
115
+ use_conv=self.use_conv,
116
+ input_fmt=self.input_fmt,
117
+ )
118
+ self.flatten = nn.Flatten(1) if self.use_conv and pool_type else nn.Identity()
119
+ else:
120
+ num_pooled_features = self.in_features * self.global_pool.feat_mult()
121
+ self.fc = _create_fc(
122
+ num_pooled_features,
123
+ num_classes,
124
+ use_conv=self.use_conv,
125
+ )
126
+
127
+ def forward(self, x, pre_logits: bool = False):
128
+ x = self.global_pool(x)
129
+ x = self.drop(x)
130
+ if pre_logits:
131
+ return self.flatten(x)
132
+ x = self.fc(x)
133
+ return self.flatten(x)
134
+
135
+
136
+ class NormMlpClassifierHead(nn.Module):
137
+ """ A Pool -> Norm -> Mlp Classifier Head for '2D' NCHW tensors
138
+ """
139
+ def __init__(
140
+ self,
141
+ in_features: int,
142
+ num_classes: int,
143
+ hidden_size: Optional[int] = None,
144
+ pool_type: str = 'avg',
145
+ drop_rate: float = 0.,
146
+ norm_layer: Union[str, Callable] = 'layernorm2d',
147
+ act_layer: Union[str, Callable] = 'tanh',
148
+ ):
149
+ """
150
+ Args:
151
+ in_features: The number of input features.
152
+ num_classes: The number of classes for the final classifier layer (output).
153
+ hidden_size: The hidden size of the MLP (pre-logits FC layer) if not None.
154
+ pool_type: Global pooling type, pooling disabled if empty string ('').
155
+ drop_rate: Pre-classifier dropout rate.
156
+ norm_layer: Normalization layer type.
157
+ act_layer: MLP activation layer type (only used if hidden_size is not None).
158
+ """
159
+ super().__init__()
160
+ self.in_features = in_features
161
+ self.hidden_size = hidden_size
162
+ self.num_features = in_features
163
+ self.use_conv = not pool_type
164
+ norm_layer = get_norm_layer(norm_layer)
165
+ act_layer = get_act_layer(act_layer)
166
+ linear_layer = partial(nn.Conv2d, kernel_size=1) if self.use_conv else nn.Linear
167
+
168
+ self.global_pool = SelectAdaptivePool2d(pool_type=pool_type)
169
+ self.norm = norm_layer(in_features)
170
+ self.flatten = nn.Flatten(1) if pool_type else nn.Identity()
171
+ if hidden_size:
172
+ self.pre_logits = nn.Sequential(OrderedDict([
173
+ ('fc', linear_layer(in_features, hidden_size)),
174
+ ('act', act_layer()),
175
+ ]))
176
+ self.num_features = hidden_size
177
+ else:
178
+ self.pre_logits = nn.Identity()
179
+ self.drop = nn.Dropout(drop_rate)
180
+ self.fc = linear_layer(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
181
+
182
+ def reset(self, num_classes: int, pool_type: Optional[str] = None):
183
+ if pool_type is not None:
184
+ self.global_pool = SelectAdaptivePool2d(pool_type=pool_type)
185
+ self.flatten = nn.Flatten(1) if pool_type else nn.Identity()
186
+ self.use_conv = self.global_pool.is_identity()
187
+ linear_layer = partial(nn.Conv2d, kernel_size=1) if self.use_conv else nn.Linear
188
+ if self.hidden_size:
189
+ if ((isinstance(self.pre_logits.fc, nn.Conv2d) and not self.use_conv) or
190
+ (isinstance(self.pre_logits.fc, nn.Linear) and self.use_conv)):
191
+ with torch.no_grad():
192
+ new_fc = linear_layer(self.in_features, self.hidden_size)
193
+ new_fc.weight.copy_(self.pre_logits.fc.weight.reshape(new_fc.weight.shape))
194
+ new_fc.bias.copy_(self.pre_logits.fc.bias)
195
+ self.pre_logits.fc = new_fc
196
+ self.fc = linear_layer(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
197
+
198
+ def forward(self, x, pre_logits: bool = False):
199
+ x = self.global_pool(x)
200
+ x = self.norm(x)
201
+ x = self.flatten(x)
202
+ x = self.pre_logits(x)
203
+ x = self.drop(x)
204
+ if pre_logits:
205
+ return x
206
+ x = self.fc(x)
207
+ return x
208
+
209
+
210
+ class ClNormMlpClassifierHead(nn.Module):
211
+ """ A Pool -> Norm -> Mlp Classifier Head for n-D NxxC tensors
212
+ """
213
+ def __init__(
214
+ self,
215
+ in_features: int,
216
+ num_classes: int,
217
+ hidden_size: Optional[int] = None,
218
+ pool_type: str = 'avg',
219
+ drop_rate: float = 0.,
220
+ norm_layer: Union[str, Callable] = 'layernorm',
221
+ act_layer: Union[str, Callable] = 'gelu',
222
+ input_fmt: str = 'NHWC',
223
+ ):
224
+ """
225
+ Args:
226
+ in_features: The number of input features.
227
+ num_classes: The number of classes for the final classifier layer (output).
228
+ hidden_size: The hidden size of the MLP (pre-logits FC layer) if not None.
229
+ pool_type: Global pooling type, pooling disabled if empty string ('').
230
+ drop_rate: Pre-classifier dropout rate.
231
+ norm_layer: Normalization layer type.
232
+ act_layer: MLP activation layer type (only used if hidden_size is not None).
233
+ """
234
+ super().__init__()
235
+ self.in_features = in_features
236
+ self.hidden_size = hidden_size
237
+ self.num_features = in_features
238
+ assert pool_type in ('', 'avg', 'max', 'avgmax')
239
+ self.pool_type = pool_type
240
+ assert input_fmt in ('NHWC', 'NLC')
241
+ self.pool_dim = 1 if input_fmt == 'NLC' else (1, 2)
242
+ norm_layer = get_norm_layer(norm_layer)
243
+ act_layer = get_act_layer(act_layer)
244
+
245
+ self.norm = norm_layer(in_features)
246
+ if hidden_size:
247
+ self.pre_logits = nn.Sequential(OrderedDict([
248
+ ('fc', nn.Linear(in_features, hidden_size)),
249
+ ('act', act_layer()),
250
+ ]))
251
+ self.num_features = hidden_size
252
+ else:
253
+ self.pre_logits = nn.Identity()
254
+ self.drop = nn.Dropout(drop_rate)
255
+ self.fc = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
256
+
257
+ def reset(self, num_classes: int, pool_type: Optional[str] = None, reset_other: bool = False):
258
+ if pool_type is not None:
259
+ self.pool_type = pool_type
260
+ if reset_other:
261
+ self.pre_logits = nn.Identity()
262
+ self.norm = nn.Identity()
263
+ self.fc = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
264
+
265
+ def _global_pool(self, x):
266
+ if self.pool_type:
267
+ if self.pool_type == 'avg':
268
+ x = x.mean(dim=self.pool_dim)
269
+ elif self.pool_type == 'max':
270
+ x = x.amax(dim=self.pool_dim)
271
+ elif self.pool_type == 'avgmax':
272
+ x = 0.5 * (x.amax(dim=self.pool_dim) + x.mean(dim=self.pool_dim))
273
+ return x
274
+
275
+ def forward(self, x, pre_logits: bool = False):
276
+ x = self._global_pool(x)
277
+ x = self.norm(x)
278
+ x = self.pre_logits(x)
279
+ x = self.drop(x)
280
+ if pre_logits:
281
+ return x
282
+ x = self.fc(x)
283
+ return x
reference_medclipseg/biomedclip/layers/cond_conv2d.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ PyTorch Conditionally Parameterized Convolution (CondConv)
2
+
3
+ Paper: CondConv: Conditionally Parameterized Convolutions for Efficient Inference
4
+ (https://arxiv.org/abs/1904.04971)
5
+
6
+ Hacked together by / Copyright 2020 Ross Wightman
7
+ """
8
+
9
+ import math
10
+ from functools import partial
11
+ import numpy as np
12
+ import torch
13
+ from torch import nn as nn
14
+ from torch.nn import functional as F
15
+
16
+ from .helpers import to_2tuple
17
+ from .conv2d_same import conv2d_same
18
+ from .padding import get_padding_value
19
+
20
+
21
+ def get_condconv_initializer(initializer, num_experts, expert_shape):
22
+ def condconv_initializer(weight):
23
+ """CondConv initializer function."""
24
+ num_params = np.prod(expert_shape)
25
+ if (len(weight.shape) != 2 or weight.shape[0] != num_experts or
26
+ weight.shape[1] != num_params):
27
+ raise (ValueError(
28
+ 'CondConv variables must have shape [num_experts, num_params]'))
29
+ for i in range(num_experts):
30
+ initializer(weight[i].view(expert_shape))
31
+ return condconv_initializer
32
+
33
+
34
+ class CondConv2d(nn.Module):
35
+ """ Conditionally Parameterized Convolution
36
+ Inspired by: https://github.com/tensorflow/tpu/blob/master/models/official/efficientnet/condconv/condconv_layers.py
37
+
38
+ Grouped convolution hackery for parallel execution of the per-sample kernel filters inspired by this discussion:
39
+ https://github.com/pytorch/pytorch/issues/17983
40
+ """
41
+ __constants__ = ['in_channels', 'out_channels', 'dynamic_padding']
42
+
43
+ def __init__(self, in_channels, out_channels, kernel_size=3,
44
+ stride=1, padding='', dilation=1, groups=1, bias=False, num_experts=4):
45
+ super(CondConv2d, self).__init__()
46
+
47
+ self.in_channels = in_channels
48
+ self.out_channels = out_channels
49
+ self.kernel_size = to_2tuple(kernel_size)
50
+ self.stride = to_2tuple(stride)
51
+ padding_val, is_padding_dynamic = get_padding_value(
52
+ padding, kernel_size, stride=stride, dilation=dilation)
53
+ self.dynamic_padding = is_padding_dynamic # if in forward to work with torchscript
54
+ self.padding = to_2tuple(padding_val)
55
+ self.dilation = to_2tuple(dilation)
56
+ self.groups = groups
57
+ self.num_experts = num_experts
58
+
59
+ self.weight_shape = (self.out_channels, self.in_channels // self.groups) + self.kernel_size
60
+ weight_num_param = 1
61
+ for wd in self.weight_shape:
62
+ weight_num_param *= wd
63
+ self.weight = torch.nn.Parameter(torch.Tensor(self.num_experts, weight_num_param))
64
+
65
+ if bias:
66
+ self.bias_shape = (self.out_channels,)
67
+ self.bias = torch.nn.Parameter(torch.Tensor(self.num_experts, self.out_channels))
68
+ else:
69
+ self.register_parameter('bias', None)
70
+
71
+ self.reset_parameters()
72
+
73
+ def reset_parameters(self):
74
+ init_weight = get_condconv_initializer(
75
+ partial(nn.init.kaiming_uniform_, a=math.sqrt(5)), self.num_experts, self.weight_shape)
76
+ init_weight(self.weight)
77
+ if self.bias is not None:
78
+ fan_in = np.prod(self.weight_shape[1:])
79
+ bound = 1 / math.sqrt(fan_in)
80
+ init_bias = get_condconv_initializer(
81
+ partial(nn.init.uniform_, a=-bound, b=bound), self.num_experts, self.bias_shape)
82
+ init_bias(self.bias)
83
+
84
+ def forward(self, x, routing_weights):
85
+ B, C, H, W = x.shape
86
+ weight = torch.matmul(routing_weights, self.weight)
87
+ new_weight_shape = (B * self.out_channels, self.in_channels // self.groups) + self.kernel_size
88
+ weight = weight.view(new_weight_shape)
89
+ bias = None
90
+ if self.bias is not None:
91
+ bias = torch.matmul(routing_weights, self.bias)
92
+ bias = bias.view(B * self.out_channels)
93
+ # move batch elements with channels so each batch element can be efficiently convolved with separate kernel
94
+ # reshape instead of view to work with channels_last input
95
+ x = x.reshape(1, B * C, H, W)
96
+ if self.dynamic_padding:
97
+ out = conv2d_same(
98
+ x, weight, bias, stride=self.stride, padding=self.padding,
99
+ dilation=self.dilation, groups=self.groups * B)
100
+ else:
101
+ out = F.conv2d(
102
+ x, weight, bias, stride=self.stride, padding=self.padding,
103
+ dilation=self.dilation, groups=self.groups * B)
104
+ out = out.permute([1, 0, 2, 3]).view(B, self.out_channels, out.shape[-2], out.shape[-1])
105
+
106
+ # Literal port (from TF definition)
107
+ # x = torch.split(x, 1, 0)
108
+ # weight = torch.split(weight, 1, 0)
109
+ # if self.bias is not None:
110
+ # bias = torch.matmul(routing_weights, self.bias)
111
+ # bias = torch.split(bias, 1, 0)
112
+ # else:
113
+ # bias = [None] * B
114
+ # out = []
115
+ # for xi, wi, bi in zip(x, weight, bias):
116
+ # wi = wi.view(*self.weight_shape)
117
+ # if bi is not None:
118
+ # bi = bi.view(*self.bias_shape)
119
+ # out.append(self.conv_fn(
120
+ # xi, wi, bi, stride=self.stride, padding=self.padding,
121
+ # dilation=self.dilation, groups=self.groups))
122
+ # out = torch.cat(out, 0)
123
+ return out
reference_medclipseg/biomedclip/layers/config.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Model / Layer Config singleton state
2
+ """
3
+ import os
4
+ import warnings
5
+ from typing import Any, Optional
6
+
7
+ import torch
8
+
9
+ __all__ = [
10
+ 'is_exportable', 'is_scriptable', 'is_no_jit', 'use_fused_attn',
11
+ 'set_exportable', 'set_scriptable', 'set_no_jit', 'set_layer_config', 'set_fused_attn',
12
+ 'set_reentrant_ckpt', 'use_reentrant_ckpt'
13
+ ]
14
+
15
+ # Set to True if prefer to have layers with no jit optimization (includes activations)
16
+ _NO_JIT = False
17
+
18
+ # Set to True if prefer to have activation layers with no jit optimization
19
+ # NOTE not currently used as no difference between no_jit and no_activation jit as only layers obeying
20
+ # the jit flags so far are activations. This will change as more layers are updated and/or added.
21
+ _NO_ACTIVATION_JIT = False
22
+
23
+ # Set to True if exporting a model with Same padding via ONNX
24
+ _EXPORTABLE = False
25
+
26
+ # Set to True if wanting to use torch.jit.script on a model
27
+ _SCRIPTABLE = False
28
+
29
+
30
+ # use torch.scaled_dot_product_attention where possible
31
+ _HAS_FUSED_ATTN = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
32
+ if 'TIMM_FUSED_ATTN' in os.environ:
33
+ _USE_FUSED_ATTN = int(os.environ['TIMM_FUSED_ATTN'])
34
+ else:
35
+ _USE_FUSED_ATTN = 1 # 0 == off, 1 == on (for tested use), 2 == on (for experimental use)
36
+
37
+
38
+ if 'TIMM_REENTRANT_CKPT' in os.environ:
39
+ _USE_REENTRANT_CKPT = bool(os.environ['TIMM_REENTRANT_CKPT'])
40
+ else:
41
+ _USE_REENTRANT_CKPT = False # defaults to disabled (off)
42
+
43
+
44
+ def is_no_jit():
45
+ return _NO_JIT
46
+
47
+
48
+ class set_no_jit:
49
+ def __init__(self, mode: bool) -> None:
50
+ global _NO_JIT
51
+ self.prev = _NO_JIT
52
+ _NO_JIT = mode
53
+
54
+ def __enter__(self) -> None:
55
+ pass
56
+
57
+ def __exit__(self, *args: Any) -> bool:
58
+ global _NO_JIT
59
+ _NO_JIT = self.prev
60
+ return False
61
+
62
+
63
+ def is_exportable():
64
+ return _EXPORTABLE
65
+
66
+
67
+ class set_exportable:
68
+ def __init__(self, mode: bool) -> None:
69
+ global _EXPORTABLE
70
+ self.prev = _EXPORTABLE
71
+ _EXPORTABLE = mode
72
+
73
+ def __enter__(self) -> None:
74
+ pass
75
+
76
+ def __exit__(self, *args: Any) -> bool:
77
+ global _EXPORTABLE
78
+ _EXPORTABLE = self.prev
79
+ return False
80
+
81
+
82
+ def is_scriptable():
83
+ return _SCRIPTABLE
84
+
85
+
86
+ class set_scriptable:
87
+ def __init__(self, mode: bool) -> None:
88
+ global _SCRIPTABLE
89
+ self.prev = _SCRIPTABLE
90
+ _SCRIPTABLE = mode
91
+
92
+ def __enter__(self) -> None:
93
+ pass
94
+
95
+ def __exit__(self, *args: Any) -> bool:
96
+ global _SCRIPTABLE
97
+ _SCRIPTABLE = self.prev
98
+ return False
99
+
100
+
101
+ class set_layer_config:
102
+ """ Layer config context manager that allows setting all layer config flags at once.
103
+ If a flag arg is None, it will not change the current value.
104
+ """
105
+ def __init__(
106
+ self,
107
+ scriptable: Optional[bool] = None,
108
+ exportable: Optional[bool] = None,
109
+ no_jit: Optional[bool] = None,
110
+ no_activation_jit: Optional[bool] = None):
111
+ global _SCRIPTABLE
112
+ global _EXPORTABLE
113
+ global _NO_JIT
114
+ global _NO_ACTIVATION_JIT
115
+ self.prev = _SCRIPTABLE, _EXPORTABLE, _NO_JIT, _NO_ACTIVATION_JIT
116
+ if scriptable is not None:
117
+ _SCRIPTABLE = scriptable
118
+ if exportable is not None:
119
+ _EXPORTABLE = exportable
120
+ if no_jit is not None:
121
+ _NO_JIT = no_jit
122
+ if no_activation_jit is not None:
123
+ _NO_ACTIVATION_JIT = no_activation_jit
124
+
125
+ def __enter__(self) -> None:
126
+ pass
127
+
128
+ def __exit__(self, *args: Any) -> bool:
129
+ global _SCRIPTABLE
130
+ global _EXPORTABLE
131
+ global _NO_JIT
132
+ global _NO_ACTIVATION_JIT
133
+ _SCRIPTABLE, _EXPORTABLE, _NO_JIT, _NO_ACTIVATION_JIT = self.prev
134
+ return False
135
+
136
+
137
+ def use_fused_attn(experimental: bool = False) -> bool:
138
+ # NOTE: ONNX export cannot handle F.scaled_dot_product_attention as of pytorch 2.0
139
+ if not _HAS_FUSED_ATTN or _EXPORTABLE:
140
+ return False
141
+ if experimental:
142
+ return _USE_FUSED_ATTN > 1
143
+ return _USE_FUSED_ATTN > 0
144
+
145
+
146
+ def set_fused_attn(enable: bool = True, experimental: bool = False):
147
+ global _USE_FUSED_ATTN
148
+ if not _HAS_FUSED_ATTN:
149
+ warnings.warn('This version of pytorch does not have F.scaled_dot_product_attention, fused_attn flag ignored.')
150
+ return
151
+ if experimental and enable:
152
+ _USE_FUSED_ATTN = 2
153
+ elif enable:
154
+ _USE_FUSED_ATTN = 1
155
+ else:
156
+ _USE_FUSED_ATTN = 0
157
+
158
+
159
+ def use_reentrant_ckpt() -> bool:
160
+ return _USE_REENTRANT_CKPT
161
+
162
+
163
+ def set_reentrant_ckpt(enable: bool = True):
164
+ global _USE_REENTRANT_CKPT
165
+ _USE_REENTRANT_CKPT = enable
reference_medclipseg/biomedclip/layers/conv2d_same.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Conv2d w/ Same Padding
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from typing import Tuple, Optional
9
+
10
+ from .config import is_exportable, is_scriptable
11
+ from .padding import pad_same, pad_same_arg, get_padding_value
12
+
13
+
14
+ _USE_EXPORT_CONV = False
15
+
16
+
17
+ def conv2d_same(
18
+ x,
19
+ weight: torch.Tensor,
20
+ bias: Optional[torch.Tensor] = None,
21
+ stride: Tuple[int, int] = (1, 1),
22
+ padding: Tuple[int, int] = (0, 0),
23
+ dilation: Tuple[int, int] = (1, 1),
24
+ groups: int = 1,
25
+ ):
26
+ x = pad_same(x, weight.shape[-2:], stride, dilation)
27
+ return F.conv2d(x, weight, bias, stride, (0, 0), dilation, groups)
28
+
29
+
30
+ class Conv2dSame(nn.Conv2d):
31
+ """ Tensorflow like 'SAME' convolution wrapper for 2D convolutions
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ in_channels,
37
+ out_channels,
38
+ kernel_size,
39
+ stride=1,
40
+ padding=0,
41
+ dilation=1,
42
+ groups=1,
43
+ bias=True,
44
+ ):
45
+ super(Conv2dSame, self).__init__(
46
+ in_channels, out_channels, kernel_size,
47
+ stride, 0, dilation, groups, bias,
48
+ )
49
+
50
+ def forward(self, x):
51
+ return conv2d_same(
52
+ x, self.weight, self.bias,
53
+ self.stride, self.padding, self.dilation, self.groups,
54
+ )
55
+
56
+
57
+ class Conv2dSameExport(nn.Conv2d):
58
+ """ ONNX export friendly Tensorflow like 'SAME' convolution wrapper for 2D convolutions
59
+
60
+ NOTE: This does not currently work with torch.jit.script
61
+ """
62
+
63
+ # pylint: disable=unused-argument
64
+ def __init__(
65
+ self,
66
+ in_channels,
67
+ out_channels,
68
+ kernel_size,
69
+ stride=1,
70
+ padding=0,
71
+ dilation=1,
72
+ groups=1,
73
+ bias=True,
74
+ ):
75
+ super(Conv2dSameExport, self).__init__(
76
+ in_channels, out_channels, kernel_size,
77
+ stride, 0, dilation, groups, bias,
78
+ )
79
+ self.pad = None
80
+ self.pad_input_size = (0, 0)
81
+
82
+ def forward(self, x):
83
+ input_size = x.size()[-2:]
84
+ if self.pad is None:
85
+ pad_arg = pad_same_arg(input_size, self.weight.size()[-2:], self.stride, self.dilation)
86
+ self.pad = nn.ZeroPad2d(pad_arg)
87
+ self.pad_input_size = input_size
88
+
89
+ x = self.pad(x)
90
+ return F.conv2d(
91
+ x, self.weight, self.bias,
92
+ self.stride, self.padding, self.dilation, self.groups,
93
+ )
94
+
95
+
96
+ def create_conv2d_pad(in_chs, out_chs, kernel_size, **kwargs):
97
+ padding = kwargs.pop('padding', '')
98
+ kwargs.setdefault('bias', False)
99
+ padding, is_dynamic = get_padding_value(padding, kernel_size, **kwargs)
100
+ if is_dynamic:
101
+ if _USE_EXPORT_CONV and is_exportable():
102
+ # older PyTorch ver needed this to export same padding reasonably
103
+ assert not is_scriptable() # Conv2DSameExport does not work with jit
104
+ return Conv2dSameExport(in_chs, out_chs, kernel_size, **kwargs)
105
+ else:
106
+ return Conv2dSame(in_chs, out_chs, kernel_size, **kwargs)
107
+ else:
108
+ return nn.Conv2d(in_chs, out_chs, kernel_size, padding=padding, **kwargs)
109
+
110
+
reference_medclipseg/biomedclip/layers/conv_bn_act.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Conv2d + BN + Act
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+ from typing import Any, Dict, Optional, Type
6
+
7
+ from torch import nn as nn
8
+
9
+ from .typing import LayerType, PadType
10
+ from .blur_pool import create_aa
11
+ from .create_conv2d import create_conv2d
12
+ from .create_norm_act import get_norm_act_layer
13
+
14
+
15
+ class ConvNormAct(nn.Module):
16
+ def __init__(
17
+ self,
18
+ in_channels: int,
19
+ out_channels: int,
20
+ kernel_size: int = 1,
21
+ stride: int = 1,
22
+ padding: PadType = '',
23
+ dilation: int = 1,
24
+ groups: int = 1,
25
+ bias: bool = False,
26
+ apply_norm: bool = True,
27
+ apply_act: bool = True,
28
+ norm_layer: LayerType = nn.BatchNorm2d,
29
+ act_layer: Optional[LayerType] = nn.ReLU,
30
+ aa_layer: Optional[LayerType] = None,
31
+ drop_layer: Optional[Type[nn.Module]] = None,
32
+ conv_kwargs: Optional[Dict[str, Any]] = None,
33
+ norm_kwargs: Optional[Dict[str, Any]] = None,
34
+ act_kwargs: Optional[Dict[str, Any]] = None,
35
+ ):
36
+ super(ConvNormAct, self).__init__()
37
+ conv_kwargs = conv_kwargs or {}
38
+ norm_kwargs = norm_kwargs or {}
39
+ act_kwargs = act_kwargs or {}
40
+ use_aa = aa_layer is not None and stride > 1
41
+
42
+ self.conv = create_conv2d(
43
+ in_channels,
44
+ out_channels,
45
+ kernel_size,
46
+ stride=1 if use_aa else stride,
47
+ padding=padding,
48
+ dilation=dilation,
49
+ groups=groups,
50
+ bias=bias,
51
+ **conv_kwargs,
52
+ )
53
+
54
+ if apply_norm:
55
+ # NOTE for backwards compatibility with models that use separate norm and act layer definitions
56
+ norm_act_layer = get_norm_act_layer(norm_layer, act_layer)
57
+ # NOTE for backwards (weight) compatibility, norm layer name remains `.bn`
58
+ if drop_layer:
59
+ norm_kwargs['drop_layer'] = drop_layer
60
+ self.bn = norm_act_layer(
61
+ out_channels,
62
+ apply_act=apply_act,
63
+ act_kwargs=act_kwargs,
64
+ **norm_kwargs,
65
+ )
66
+ else:
67
+ self.bn = nn.Sequential()
68
+ if drop_layer:
69
+ norm_kwargs['drop_layer'] = drop_layer
70
+ self.bn.add_module('drop', drop_layer())
71
+
72
+ self.aa = create_aa(aa_layer, out_channels, stride=stride, enable=use_aa, noop=None)
73
+
74
+ @property
75
+ def in_channels(self):
76
+ return self.conv.in_channels
77
+
78
+ @property
79
+ def out_channels(self):
80
+ return self.conv.out_channels
81
+
82
+ def forward(self, x):
83
+ x = self.conv(x)
84
+ x = self.bn(x)
85
+ aa = getattr(self, 'aa', None)
86
+ if aa is not None:
87
+ x = self.aa(x)
88
+ return x
89
+
90
+
91
+ ConvBnAct = ConvNormAct
92
+ ConvNormActAa = ConvNormAct # backwards compat, when they were separate
reference_medclipseg/biomedclip/layers/create_act.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Activation Factory
2
+ Hacked together by / Copyright 2020 Ross Wightman
3
+ """
4
+ from typing import Union, Callable, Type
5
+
6
+ from .activations import *
7
+ from .activations_me import *
8
+ from .config import is_exportable, is_scriptable
9
+
10
+ # PyTorch has an optimized, native 'silu' (aka 'swish') operator as of PyTorch 1.7.
11
+ # Also hardsigmoid, hardswish, and soon mish. This code will use native version if present.
12
+ # Eventually, the custom SiLU, Mish, Hard*, layers will be removed and only native variants will be used.
13
+ _has_silu = 'silu' in dir(torch.nn.functional)
14
+ _has_hardswish = 'hardswish' in dir(torch.nn.functional)
15
+ _has_hardsigmoid = 'hardsigmoid' in dir(torch.nn.functional)
16
+ _has_mish = 'mish' in dir(torch.nn.functional)
17
+
18
+
19
+ _ACT_FN_DEFAULT = dict(
20
+ silu=F.silu if _has_silu else swish,
21
+ swish=F.silu if _has_silu else swish,
22
+ mish=F.mish if _has_mish else mish,
23
+ relu=F.relu,
24
+ relu6=F.relu6,
25
+ leaky_relu=F.leaky_relu,
26
+ elu=F.elu,
27
+ celu=F.celu,
28
+ selu=F.selu,
29
+ gelu=gelu,
30
+ gelu_tanh=gelu_tanh,
31
+ quick_gelu=quick_gelu,
32
+ sigmoid=sigmoid,
33
+ tanh=tanh,
34
+ hard_sigmoid=F.hardsigmoid if _has_hardsigmoid else hard_sigmoid,
35
+ hard_swish=F.hardswish if _has_hardswish else hard_swish,
36
+ hard_mish=hard_mish,
37
+ )
38
+
39
+ _ACT_FN_ME = dict(
40
+ silu=F.silu if _has_silu else swish_me,
41
+ swish=F.silu if _has_silu else swish_me,
42
+ mish=F.mish if _has_mish else mish_me,
43
+ hard_sigmoid=F.hardsigmoid if _has_hardsigmoid else hard_sigmoid_me,
44
+ hard_swish=F.hardswish if _has_hardswish else hard_swish_me,
45
+ hard_mish=hard_mish_me,
46
+ )
47
+
48
+ _ACT_FNS = (_ACT_FN_ME, _ACT_FN_DEFAULT)
49
+ for a in _ACT_FNS:
50
+ a.setdefault('hardsigmoid', a.get('hard_sigmoid'))
51
+ a.setdefault('hardswish', a.get('hard_swish'))
52
+
53
+
54
+ _ACT_LAYER_DEFAULT = dict(
55
+ silu=nn.SiLU if _has_silu else Swish,
56
+ swish=nn.SiLU if _has_silu else Swish,
57
+ mish=nn.Mish if _has_mish else Mish,
58
+ relu=nn.ReLU,
59
+ relu6=nn.ReLU6,
60
+ leaky_relu=nn.LeakyReLU,
61
+ elu=nn.ELU,
62
+ prelu=PReLU,
63
+ celu=nn.CELU,
64
+ selu=nn.SELU,
65
+ gelu=GELU,
66
+ gelu_tanh=GELUTanh,
67
+ quick_gelu=QuickGELU,
68
+ sigmoid=Sigmoid,
69
+ tanh=Tanh,
70
+ hard_sigmoid=nn.Hardsigmoid if _has_hardsigmoid else HardSigmoid,
71
+ hard_swish=nn.Hardswish if _has_hardswish else HardSwish,
72
+ hard_mish=HardMish,
73
+ identity=nn.Identity,
74
+ )
75
+
76
+ _ACT_LAYER_ME = dict(
77
+ silu=nn.SiLU if _has_silu else SwishMe,
78
+ swish=nn.SiLU if _has_silu else SwishMe,
79
+ mish=nn.Mish if _has_mish else MishMe,
80
+ hard_sigmoid=nn.Hardsigmoid if _has_hardsigmoid else HardSigmoidMe,
81
+ hard_swish=nn.Hardswish if _has_hardswish else HardSwishMe,
82
+ hard_mish=HardMishMe,
83
+ )
84
+
85
+ _ACT_LAYERS = (_ACT_LAYER_ME, _ACT_LAYER_DEFAULT)
86
+ for a in _ACT_LAYERS:
87
+ a.setdefault('hardsigmoid', a.get('hard_sigmoid'))
88
+ a.setdefault('hardswish', a.get('hard_swish'))
89
+
90
+
91
+ def get_act_fn(name: Union[Callable, str] = 'relu'):
92
+ """ Activation Function Factory
93
+ Fetching activation fns by name with this function allows export or torch script friendly
94
+ functions to be returned dynamically based on current config.
95
+ """
96
+ if not name:
97
+ return None
98
+ if isinstance(name, Callable):
99
+ return name
100
+ name = name.lower()
101
+ if not (is_exportable() or is_scriptable()):
102
+ # If not exporting or scripting the model, first look for a memory-efficient version with
103
+ # custom autograd, then fallback
104
+ if name in _ACT_FN_ME:
105
+ return _ACT_FN_ME[name]
106
+ return _ACT_FN_DEFAULT[name]
107
+
108
+
109
+ def get_act_layer(name: Union[Type[nn.Module], str] = 'relu'):
110
+ """ Activation Layer Factory
111
+ Fetching activation layers by name with this function allows export or torch script friendly
112
+ functions to be returned dynamically based on current config.
113
+ """
114
+ if name is None:
115
+ return None
116
+ if not isinstance(name, str):
117
+ # callable, module, etc
118
+ return name
119
+ if not name:
120
+ return None
121
+ name = name.lower()
122
+ if not (is_exportable() or is_scriptable()):
123
+ if name in _ACT_LAYER_ME:
124
+ return _ACT_LAYER_ME[name]
125
+ return _ACT_LAYER_DEFAULT[name]
126
+
127
+
128
+ def create_act_layer(name: Union[Type[nn.Module], str], inplace=None, **kwargs):
129
+ act_layer = get_act_layer(name)
130
+ if act_layer is None:
131
+ return None
132
+ if inplace is None:
133
+ return act_layer(**kwargs)
134
+ try:
135
+ return act_layer(inplace=inplace, **kwargs)
136
+ except TypeError:
137
+ # recover if act layer doesn't have inplace arg
138
+ return act_layer(**kwargs)
reference_medclipseg/biomedclip/layers/create_attn.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Attention Factory
2
+
3
+ Hacked together by / Copyright 2021 Ross Wightman
4
+ """
5
+ import torch
6
+ from functools import partial
7
+
8
+ from .bottleneck_attn import BottleneckAttn
9
+ from .cbam import CbamModule, LightCbamModule
10
+ from .eca import EcaModule, CecaModule
11
+ from .gather_excite import GatherExcite
12
+ from .global_context import GlobalContext
13
+ from .halo_attn import HaloAttn
14
+ from .lambda_layer import LambdaLayer
15
+ from .non_local_attn import NonLocalAttn, BatNonLocalAttn
16
+ from .selective_kernel import SelectiveKernel
17
+ from .split_attn import SplitAttn
18
+ from .squeeze_excite import SEModule, EffectiveSEModule
19
+
20
+
21
+ def get_attn(attn_type):
22
+ if isinstance(attn_type, torch.nn.Module):
23
+ return attn_type
24
+ module_cls = None
25
+ if attn_type:
26
+ if isinstance(attn_type, str):
27
+ attn_type = attn_type.lower()
28
+ # Lightweight attention modules (channel and/or coarse spatial).
29
+ # Typically added to existing network architecture blocks in addition to existing convolutions.
30
+ if attn_type == 'se':
31
+ module_cls = SEModule
32
+ elif attn_type == 'ese':
33
+ module_cls = EffectiveSEModule
34
+ elif attn_type == 'eca':
35
+ module_cls = EcaModule
36
+ elif attn_type == 'ecam':
37
+ module_cls = partial(EcaModule, use_mlp=True)
38
+ elif attn_type == 'ceca':
39
+ module_cls = CecaModule
40
+ elif attn_type == 'ge':
41
+ module_cls = GatherExcite
42
+ elif attn_type == 'gc':
43
+ module_cls = GlobalContext
44
+ elif attn_type == 'gca':
45
+ module_cls = partial(GlobalContext, fuse_add=True, fuse_scale=False)
46
+ elif attn_type == 'cbam':
47
+ module_cls = CbamModule
48
+ elif attn_type == 'lcbam':
49
+ module_cls = LightCbamModule
50
+
51
+ # Attention / attention-like modules w/ significant params
52
+ # Typically replace some of the existing workhorse convs in a network architecture.
53
+ # All of these accept a stride argument and can spatially downsample the input.
54
+ elif attn_type == 'sk':
55
+ module_cls = SelectiveKernel
56
+ elif attn_type == 'splat':
57
+ module_cls = SplitAttn
58
+
59
+ # Self-attention / attention-like modules w/ significant compute and/or params
60
+ # Typically replace some of the existing workhorse convs in a network architecture.
61
+ # All of these accept a stride argument and can spatially downsample the input.
62
+ elif attn_type == 'lambda':
63
+ return LambdaLayer
64
+ elif attn_type == 'bottleneck':
65
+ return BottleneckAttn
66
+ elif attn_type == 'halo':
67
+ return HaloAttn
68
+ elif attn_type == 'nl':
69
+ module_cls = NonLocalAttn
70
+ elif attn_type == 'bat':
71
+ module_cls = BatNonLocalAttn
72
+
73
+ # Woops!
74
+ else:
75
+ assert False, "Invalid attn module (%s)" % attn_type
76
+ elif isinstance(attn_type, bool):
77
+ if attn_type:
78
+ module_cls = SEModule
79
+ else:
80
+ module_cls = attn_type
81
+ return module_cls
82
+
83
+
84
+ def create_attn(attn_type, channels, **kwargs):
85
+ module_cls = get_attn(attn_type)
86
+ if module_cls is not None:
87
+ # NOTE: it's expected the first (positional) argument of all attention layers is the # input channels
88
+ return module_cls(channels, **kwargs)
89
+ return None
reference_medclipseg/biomedclip/layers/create_conv2d.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Create Conv2d Factory Method
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+
6
+ from .mixed_conv2d import MixedConv2d
7
+ from .cond_conv2d import CondConv2d
8
+ from .conv2d_same import create_conv2d_pad
9
+
10
+
11
+ def create_conv2d(in_channels, out_channels, kernel_size, **kwargs):
12
+ """ Select a 2d convolution implementation based on arguments
13
+ Creates and returns one of torch.nn.Conv2d, Conv2dSame, MixedConv2d, or CondConv2d.
14
+
15
+ Used extensively by EfficientNet, MobileNetv3 and related networks.
16
+ """
17
+ if isinstance(kernel_size, list):
18
+ assert 'num_experts' not in kwargs # MixNet + CondConv combo not supported currently
19
+ if 'groups' in kwargs:
20
+ groups = kwargs.pop('groups')
21
+ if groups == in_channels:
22
+ kwargs['depthwise'] = True
23
+ else:
24
+ assert groups == 1
25
+ # We're going to use only lists for defining the MixedConv2d kernel groups,
26
+ # ints, tuples, other iterables will continue to pass to normal conv and specify h, w.
27
+ m = MixedConv2d(in_channels, out_channels, kernel_size, **kwargs)
28
+ else:
29
+ depthwise = kwargs.pop('depthwise', False)
30
+ # for DW out_channels must be multiple of in_channels as must have out_channels % groups == 0
31
+ groups = in_channels if depthwise else kwargs.pop('groups', 1)
32
+ if 'num_experts' in kwargs and kwargs['num_experts'] > 0:
33
+ m = CondConv2d(in_channels, out_channels, kernel_size, groups=groups, **kwargs)
34
+ else:
35
+ m = create_conv2d_pad(in_channels, out_channels, kernel_size, groups=groups, **kwargs)
36
+ return m
reference_medclipseg/biomedclip/layers/create_norm.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Norm Layer Factory
2
+
3
+ Create norm modules by string (to mirror create_act and creat_norm-act fns)
4
+
5
+ Copyright 2022 Ross Wightman
6
+ """
7
+ import functools
8
+ import types
9
+ from typing import Type
10
+
11
+ import torch.nn as nn
12
+
13
+ from .norm import GroupNorm, GroupNorm1, LayerNorm, LayerNorm2d, RmsNorm, RmsNorm2d, SimpleNorm, SimpleNorm2d
14
+ from torchvision.ops.misc import FrozenBatchNorm2d
15
+
16
+ _NORM_MAP = dict(
17
+ batchnorm=nn.BatchNorm2d,
18
+ batchnorm2d=nn.BatchNorm2d,
19
+ batchnorm1d=nn.BatchNorm1d,
20
+ groupnorm=GroupNorm,
21
+ groupnorm1=GroupNorm1,
22
+ layernorm=LayerNorm,
23
+ layernorm2d=LayerNorm2d,
24
+ rmsnorm=RmsNorm,
25
+ rmsnorm2d=RmsNorm2d,
26
+ simplenorm=SimpleNorm,
27
+ simplenorm2d=SimpleNorm2d,
28
+ frozenbatchnorm2d=FrozenBatchNorm2d,
29
+ )
30
+ _NORM_TYPES = {m for n, m in _NORM_MAP.items()}
31
+
32
+
33
+ def create_norm_layer(layer_name, num_features, **kwargs):
34
+ layer = get_norm_layer(layer_name)
35
+ layer_instance = layer(num_features, **kwargs)
36
+ return layer_instance
37
+
38
+
39
+ def get_norm_layer(norm_layer):
40
+ if norm_layer is None:
41
+ return None
42
+ assert isinstance(norm_layer, (type, str, types.FunctionType, functools.partial))
43
+ norm_kwargs = {}
44
+
45
+ # unbind partial fn, so args can be rebound later
46
+ if isinstance(norm_layer, functools.partial):
47
+ norm_kwargs.update(norm_layer.keywords)
48
+ norm_layer = norm_layer.func
49
+
50
+ if isinstance(norm_layer, str):
51
+ if not norm_layer:
52
+ return None
53
+ layer_name = norm_layer.replace('_', '').lower()
54
+ norm_layer = _NORM_MAP[layer_name]
55
+ else:
56
+ norm_layer = norm_layer
57
+
58
+ if norm_kwargs:
59
+ norm_layer = functools.partial(norm_layer, **norm_kwargs) # bind/rebind args
60
+ return norm_layer
reference_medclipseg/biomedclip/layers/create_norm_act.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ NormAct (Normalization + Activation Layer) Factory
2
+
3
+ Create norm + act combo modules that attempt to be backwards compatible with separate norm + act
4
+ instances in models. Where these are used it will be possible to swap separate BN + act layers with
5
+ combined modules like IABN or EvoNorms.
6
+
7
+ Hacked together by / Copyright 2020 Ross Wightman
8
+ """
9
+ import types
10
+ import functools
11
+
12
+ from .evo_norm import *
13
+ from .filter_response_norm import FilterResponseNormAct2d, FilterResponseNormTlu2d
14
+ from .norm_act import BatchNormAct2d, GroupNormAct, LayerNormAct, LayerNormAct2d
15
+ from .inplace_abn import InplaceAbn
16
+
17
+ _NORM_ACT_MAP = dict(
18
+ batchnorm=BatchNormAct2d,
19
+ batchnorm2d=BatchNormAct2d,
20
+ groupnorm=GroupNormAct,
21
+ groupnorm1=functools.partial(GroupNormAct, num_groups=1),
22
+ layernorm=LayerNormAct,
23
+ layernorm2d=LayerNormAct2d,
24
+ evonormb0=EvoNorm2dB0,
25
+ evonormb1=EvoNorm2dB1,
26
+ evonormb2=EvoNorm2dB2,
27
+ evonorms0=EvoNorm2dS0,
28
+ evonorms0a=EvoNorm2dS0a,
29
+ evonorms1=EvoNorm2dS1,
30
+ evonorms1a=EvoNorm2dS1a,
31
+ evonorms2=EvoNorm2dS2,
32
+ evonorms2a=EvoNorm2dS2a,
33
+ frn=FilterResponseNormAct2d,
34
+ frntlu=FilterResponseNormTlu2d,
35
+ inplaceabn=InplaceAbn,
36
+ iabn=InplaceAbn,
37
+ )
38
+ _NORM_ACT_TYPES = {m for n, m in _NORM_ACT_MAP.items()}
39
+ # has act_layer arg to define act type
40
+ _NORM_ACT_REQUIRES_ARG = {
41
+ BatchNormAct2d, GroupNormAct, LayerNormAct, LayerNormAct2d, FilterResponseNormAct2d, InplaceAbn}
42
+
43
+
44
+ def create_norm_act_layer(layer_name, num_features, act_layer=None, apply_act=True, jit=False, **kwargs):
45
+ layer = get_norm_act_layer(layer_name, act_layer=act_layer)
46
+ layer_instance = layer(num_features, apply_act=apply_act, **kwargs)
47
+ if jit:
48
+ layer_instance = torch.jit.script(layer_instance)
49
+ return layer_instance
50
+
51
+
52
+ def get_norm_act_layer(norm_layer, act_layer=None):
53
+ if norm_layer is None:
54
+ return None
55
+ assert isinstance(norm_layer, (type, str, types.FunctionType, functools.partial))
56
+ assert act_layer is None or isinstance(act_layer, (type, str, types.FunctionType, functools.partial))
57
+ norm_act_kwargs = {}
58
+
59
+ # unbind partial fn, so args can be rebound later
60
+ if isinstance(norm_layer, functools.partial):
61
+ norm_act_kwargs.update(norm_layer.keywords)
62
+ norm_layer = norm_layer.func
63
+
64
+ if isinstance(norm_layer, str):
65
+ if not norm_layer:
66
+ return None
67
+ layer_name = norm_layer.replace('_', '').lower().split('-')[0]
68
+ norm_act_layer = _NORM_ACT_MAP[layer_name]
69
+ elif norm_layer in _NORM_ACT_TYPES:
70
+ norm_act_layer = norm_layer
71
+ elif isinstance(norm_layer, types.FunctionType):
72
+ # if function type, must be a lambda/fn that creates a norm_act layer
73
+ norm_act_layer = norm_layer
74
+ else:
75
+ type_name = norm_layer.__name__.lower()
76
+ if type_name.startswith('batchnorm'):
77
+ norm_act_layer = BatchNormAct2d
78
+ elif type_name.startswith('groupnorm'):
79
+ norm_act_layer = GroupNormAct
80
+ elif type_name.startswith('groupnorm1'):
81
+ norm_act_layer = functools.partial(GroupNormAct, num_groups=1)
82
+ elif type_name.startswith('layernorm2d'):
83
+ norm_act_layer = LayerNormAct2d
84
+ elif type_name.startswith('layernorm'):
85
+ norm_act_layer = LayerNormAct
86
+ else:
87
+ assert False, f"No equivalent norm_act layer for {type_name}"
88
+
89
+ if norm_act_layer in _NORM_ACT_REQUIRES_ARG:
90
+ # pass `act_layer` through for backwards compat where `act_layer=None` implies no activation.
91
+ # In the future, may force use of `apply_act` with `act_layer` arg bound to relevant NormAct types
92
+ norm_act_kwargs.setdefault('act_layer', act_layer)
93
+ if norm_act_kwargs:
94
+ norm_act_layer = functools.partial(norm_act_layer, **norm_act_kwargs) # bind/rebind args
95
+ return norm_act_layer
reference_medclipseg/biomedclip/layers/drop.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ DropBlock, DropPath
2
+
3
+ PyTorch implementations of DropBlock and DropPath (Stochastic Depth) regularization layers.
4
+
5
+ Papers:
6
+ DropBlock: A regularization method for convolutional networks (https://arxiv.org/abs/1810.12890)
7
+
8
+ Deep Networks with Stochastic Depth (https://arxiv.org/abs/1603.09382)
9
+
10
+ Code:
11
+ DropBlock impl inspired by two Tensorflow impl that I liked:
12
+ - https://github.com/tensorflow/tpu/blob/master/models/official/resnet/resnet_model.py#L74
13
+ - https://github.com/clovaai/assembled-cnn/blob/master/nets/blocks.py
14
+
15
+ Hacked together by / Copyright 2020 Ross Wightman
16
+ """
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+
21
+ from .grid import ndgrid
22
+
23
+
24
+ def drop_block_2d(
25
+ x,
26
+ drop_prob: float = 0.1,
27
+ block_size: int = 7,
28
+ gamma_scale: float = 1.0,
29
+ with_noise: bool = False,
30
+ inplace: bool = False,
31
+ batchwise: bool = False
32
+ ):
33
+ """ DropBlock. See https://arxiv.org/pdf/1810.12890.pdf
34
+
35
+ DropBlock with an experimental gaussian noise option. This layer has been tested on a few training
36
+ runs with success, but needs further validation and possibly optimization for lower runtime impact.
37
+ """
38
+ B, C, H, W = x.shape
39
+ total_size = W * H
40
+ clipped_block_size = min(block_size, min(W, H))
41
+ # seed_drop_rate, the gamma parameter
42
+ gamma = gamma_scale * drop_prob * total_size / clipped_block_size ** 2 / (
43
+ (W - block_size + 1) * (H - block_size + 1))
44
+
45
+ # Forces the block to be inside the feature map.
46
+ w_i, h_i = ndgrid(torch.arange(W, device=x.device), torch.arange(H, device=x.device))
47
+ valid_block = ((w_i >= clipped_block_size // 2) & (w_i < W - (clipped_block_size - 1) // 2)) & \
48
+ ((h_i >= clipped_block_size // 2) & (h_i < H - (clipped_block_size - 1) // 2))
49
+ valid_block = torch.reshape(valid_block, (1, 1, H, W)).to(dtype=x.dtype)
50
+
51
+ if batchwise:
52
+ # one mask for whole batch, quite a bit faster
53
+ uniform_noise = torch.rand((1, C, H, W), dtype=x.dtype, device=x.device)
54
+ else:
55
+ uniform_noise = torch.rand_like(x)
56
+ block_mask = ((2 - gamma - valid_block + uniform_noise) >= 1).to(dtype=x.dtype)
57
+ block_mask = -F.max_pool2d(
58
+ -block_mask,
59
+ kernel_size=clipped_block_size, # block_size,
60
+ stride=1,
61
+ padding=clipped_block_size // 2)
62
+
63
+ if with_noise:
64
+ normal_noise = torch.randn((1, C, H, W), dtype=x.dtype, device=x.device) if batchwise else torch.randn_like(x)
65
+ if inplace:
66
+ x.mul_(block_mask).add_(normal_noise * (1 - block_mask))
67
+ else:
68
+ x = x * block_mask + normal_noise * (1 - block_mask)
69
+ else:
70
+ normalize_scale = (block_mask.numel() / block_mask.to(dtype=torch.float32).sum().add(1e-7)).to(x.dtype)
71
+ if inplace:
72
+ x.mul_(block_mask * normalize_scale)
73
+ else:
74
+ x = x * block_mask * normalize_scale
75
+ return x
76
+
77
+
78
+ def drop_block_fast_2d(
79
+ x: torch.Tensor,
80
+ drop_prob: float = 0.1,
81
+ block_size: int = 7,
82
+ gamma_scale: float = 1.0,
83
+ with_noise: bool = False,
84
+ inplace: bool = False,
85
+ ):
86
+ """ DropBlock. See https://arxiv.org/pdf/1810.12890.pdf
87
+
88
+ DropBlock with an experimental gaussian noise option. Simplied from above without concern for valid
89
+ block mask at edges.
90
+ """
91
+ B, C, H, W = x.shape
92
+ total_size = W * H
93
+ clipped_block_size = min(block_size, min(W, H))
94
+ gamma = gamma_scale * drop_prob * total_size / clipped_block_size ** 2 / (
95
+ (W - block_size + 1) * (H - block_size + 1))
96
+
97
+ block_mask = torch.empty_like(x).bernoulli_(gamma)
98
+ block_mask = F.max_pool2d(
99
+ block_mask.to(x.dtype), kernel_size=clipped_block_size, stride=1, padding=clipped_block_size // 2)
100
+
101
+ if with_noise:
102
+ normal_noise = torch.empty_like(x).normal_()
103
+ if inplace:
104
+ x.mul_(1. - block_mask).add_(normal_noise * block_mask)
105
+ else:
106
+ x = x * (1. - block_mask) + normal_noise * block_mask
107
+ else:
108
+ block_mask = 1 - block_mask
109
+ normalize_scale = (block_mask.numel() / block_mask.to(dtype=torch.float32).sum().add(1e-6)).to(dtype=x.dtype)
110
+ if inplace:
111
+ x.mul_(block_mask * normalize_scale)
112
+ else:
113
+ x = x * block_mask * normalize_scale
114
+ return x
115
+
116
+
117
+ class DropBlock2d(nn.Module):
118
+ """ DropBlock. See https://arxiv.org/pdf/1810.12890.pdf
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ drop_prob: float = 0.1,
124
+ block_size: int = 7,
125
+ gamma_scale: float = 1.0,
126
+ with_noise: bool = False,
127
+ inplace: bool = False,
128
+ batchwise: bool = False,
129
+ fast: bool = True):
130
+ super(DropBlock2d, self).__init__()
131
+ self.drop_prob = drop_prob
132
+ self.gamma_scale = gamma_scale
133
+ self.block_size = block_size
134
+ self.with_noise = with_noise
135
+ self.inplace = inplace
136
+ self.batchwise = batchwise
137
+ self.fast = fast # FIXME finish comparisons of fast vs not
138
+
139
+ def forward(self, x):
140
+ if not self.training or not self.drop_prob:
141
+ return x
142
+ if self.fast:
143
+ return drop_block_fast_2d(
144
+ x, self.drop_prob, self.block_size, self.gamma_scale, self.with_noise, self.inplace)
145
+ else:
146
+ return drop_block_2d(
147
+ x, self.drop_prob, self.block_size, self.gamma_scale, self.with_noise, self.inplace, self.batchwise)
148
+
149
+
150
+ def drop_path(x, drop_prob: float = 0., training: bool = False, scale_by_keep: bool = True):
151
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
152
+
153
+ This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
154
+ the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
155
+ See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
156
+ changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
157
+ 'survival rate' as the argument.
158
+
159
+ """
160
+ if drop_prob == 0. or not training:
161
+ return x
162
+ keep_prob = 1 - drop_prob
163
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
164
+ random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
165
+ if keep_prob > 0.0 and scale_by_keep:
166
+ random_tensor.div_(keep_prob)
167
+ return x * random_tensor
168
+
169
+
170
+ class DropPath(nn.Module):
171
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
172
+ """
173
+ def __init__(self, drop_prob: float = 0., scale_by_keep: bool = True):
174
+ super(DropPath, self).__init__()
175
+ self.drop_prob = drop_prob
176
+ self.scale_by_keep = scale_by_keep
177
+
178
+ def forward(self, x):
179
+ return drop_path(x, self.drop_prob, self.training, self.scale_by_keep)
180
+
181
+ def extra_repr(self):
182
+ return f'drop_prob={round(self.drop_prob,3):0.3f}'
reference_medclipseg/biomedclip/layers/eca.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ECA module from ECAnet
3
+
4
+ paper: ECA-Net: Efficient Channel Attention for Deep Convolutional Neural Networks
5
+ https://arxiv.org/abs/1910.03151
6
+
7
+ Original ECA model borrowed from https://github.com/BangguWu/ECANet
8
+
9
+ Modified circular ECA implementation and adaption for use in timm package
10
+ by Chris Ha https://github.com/VRandme
11
+
12
+ Original License:
13
+
14
+ MIT License
15
+
16
+ Copyright (c) 2019 BangguWu, Qilong Wang
17
+
18
+ Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ of this software and associated documentation files (the "Software"), to deal
20
+ in the Software without restriction, including without limitation the rights
21
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ copies of the Software, and to permit persons to whom the Software is
23
+ furnished to do so, subject to the following conditions:
24
+
25
+ The above copyright notice and this permission notice shall be included in all
26
+ copies or substantial portions of the Software.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34
+ SOFTWARE.
35
+ """
36
+ import math
37
+ from torch import nn
38
+ import torch.nn.functional as F
39
+
40
+
41
+ from .create_act import create_act_layer
42
+ from .helpers import make_divisible
43
+
44
+
45
+ class EcaModule(nn.Module):
46
+ """Constructs an ECA module.
47
+
48
+ Args:
49
+ channels: Number of channels of the input feature map for use in adaptive kernel sizes
50
+ for actual calculations according to channel.
51
+ gamma, beta: when channel is given parameters of mapping function
52
+ refer to original paper https://arxiv.org/pdf/1910.03151.pdf
53
+ (default=None. if channel size not given, use k_size given for kernel size.)
54
+ kernel_size: Adaptive selection of kernel size (default=3)
55
+ gamm: used in kernel_size calc, see above
56
+ beta: used in kernel_size calc, see above
57
+ act_layer: optional non-linearity after conv, enables conv bias, this is an experiment
58
+ gate_layer: gating non-linearity to use
59
+ """
60
+ def __init__(
61
+ self, channels=None, kernel_size=3, gamma=2, beta=1, act_layer=None, gate_layer='sigmoid',
62
+ rd_ratio=1/8, rd_channels=None, rd_divisor=8, use_mlp=False):
63
+ super(EcaModule, self).__init__()
64
+ if channels is not None:
65
+ t = int(abs(math.log(channels, 2) + beta) / gamma)
66
+ kernel_size = max(t if t % 2 else t + 1, 3)
67
+ assert kernel_size % 2 == 1
68
+ padding = (kernel_size - 1) // 2
69
+ if use_mlp:
70
+ # NOTE 'mlp' mode is a timm experiment, not in paper
71
+ assert channels is not None
72
+ if rd_channels is None:
73
+ rd_channels = make_divisible(channels * rd_ratio, divisor=rd_divisor)
74
+ act_layer = act_layer or nn.ReLU
75
+ self.conv = nn.Conv1d(1, rd_channels, kernel_size=1, padding=0, bias=True)
76
+ self.act = create_act_layer(act_layer)
77
+ self.conv2 = nn.Conv1d(rd_channels, 1, kernel_size=kernel_size, padding=padding, bias=True)
78
+ else:
79
+ self.conv = nn.Conv1d(1, 1, kernel_size=kernel_size, padding=padding, bias=False)
80
+ self.act = None
81
+ self.conv2 = None
82
+ self.gate = create_act_layer(gate_layer)
83
+
84
+ def forward(self, x):
85
+ y = x.mean((2, 3)).view(x.shape[0], 1, -1) # view for 1d conv
86
+ y = self.conv(y)
87
+ if self.conv2 is not None:
88
+ y = self.act(y)
89
+ y = self.conv2(y)
90
+ y = self.gate(y).view(x.shape[0], -1, 1, 1)
91
+ return x * y.expand_as(x)
92
+
93
+
94
+ EfficientChannelAttn = EcaModule # alias
95
+
96
+
97
+ class CecaModule(nn.Module):
98
+ """Constructs a circular ECA module.
99
+
100
+ ECA module where the conv uses circular padding rather than zero padding.
101
+ Unlike the spatial dimension, the channels do not have inherent ordering nor
102
+ locality. Although this module in essence, applies such an assumption, it is unnecessary
103
+ to limit the channels on either "edge" from being circularly adapted to each other.
104
+ This will fundamentally increase connectivity and possibly increase performance metrics
105
+ (accuracy, robustness), without significantly impacting resource metrics
106
+ (parameter size, throughput,latency, etc)
107
+
108
+ Args:
109
+ channels: Number of channels of the input feature map for use in adaptive kernel sizes
110
+ for actual calculations according to channel.
111
+ gamma, beta: when channel is given parameters of mapping function
112
+ refer to original paper https://arxiv.org/pdf/1910.03151.pdf
113
+ (default=None. if channel size not given, use k_size given for kernel size.)
114
+ kernel_size: Adaptive selection of kernel size (default=3)
115
+ gamm: used in kernel_size calc, see above
116
+ beta: used in kernel_size calc, see above
117
+ act_layer: optional non-linearity after conv, enables conv bias, this is an experiment
118
+ gate_layer: gating non-linearity to use
119
+ """
120
+
121
+ def __init__(self, channels=None, kernel_size=3, gamma=2, beta=1, act_layer=None, gate_layer='sigmoid'):
122
+ super(CecaModule, self).__init__()
123
+ if channels is not None:
124
+ t = int(abs(math.log(channels, 2) + beta) / gamma)
125
+ kernel_size = max(t if t % 2 else t + 1, 3)
126
+ has_act = act_layer is not None
127
+ assert kernel_size % 2 == 1
128
+
129
+ # PyTorch circular padding mode is buggy as of pytorch 1.4
130
+ # see https://github.com/pytorch/pytorch/pull/17240
131
+ # implement manual circular padding
132
+ self.padding = (kernel_size - 1) // 2
133
+ self.conv = nn.Conv1d(1, 1, kernel_size=kernel_size, padding=0, bias=has_act)
134
+ self.gate = create_act_layer(gate_layer)
135
+
136
+ def forward(self, x):
137
+ y = x.mean((2, 3)).view(x.shape[0], 1, -1)
138
+ # Manually implement circular padding, F.pad does not seemed to be bugged
139
+ y = F.pad(y, (self.padding, self.padding), mode='circular')
140
+ y = self.conv(y)
141
+ y = self.gate(y).view(x.shape[0], -1, 1, 1)
142
+ return x * y.expand_as(x)
143
+
144
+
145
+ CircularEfficientChannelAttn = CecaModule
reference_medclipseg/biomedclip/layers/evo_norm.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ EvoNorm in PyTorch
2
+
3
+ Based on `Evolving Normalization-Activation Layers` - https://arxiv.org/abs/2004.02967
4
+ @inproceedings{NEURIPS2020,
5
+ author = {Liu, Hanxiao and Brock, Andy and Simonyan, Karen and Le, Quoc},
6
+ booktitle = {Advances in Neural Information Processing Systems},
7
+ editor = {H. Larochelle and M. Ranzato and R. Hadsell and M. F. Balcan and H. Lin},
8
+ pages = {13539--13550},
9
+ publisher = {Curran Associates, Inc.},
10
+ title = {Evolving Normalization-Activation Layers},
11
+ url = {https://proceedings.neurips.cc/paper/2020/file/9d4c03631b8b0c85ae08bf05eda37d0f-Paper.pdf},
12
+ volume = {33},
13
+ year = {2020}
14
+ }
15
+
16
+ An attempt at getting decent performing EvoNorms running in PyTorch.
17
+ While faster than other PyTorch impl, still quite a ways off the built-in BatchNorm
18
+ in terms of memory usage and throughput on GPUs.
19
+
20
+ I'm testing these modules on TPU w/ PyTorch XLA. Promising start but
21
+ currently working around some issues with builtin torch/tensor.var/std. Unlike
22
+ GPU, similar train speeds for EvoNormS variants and BatchNorm.
23
+
24
+ Hacked together by / Copyright 2020 Ross Wightman
25
+ """
26
+ from typing import Sequence, Union
27
+
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+
32
+ from .create_act import create_act_layer
33
+ from .trace_utils import _assert
34
+
35
+
36
+ def instance_std(x, eps: float = 1e-5):
37
+ std = x.float().var(dim=(2, 3), unbiased=False, keepdim=True).add(eps).sqrt().to(x.dtype)
38
+ return std.expand(x.shape)
39
+
40
+
41
+ def instance_std_tpu(x, eps: float = 1e-5):
42
+ std = manual_var(x, dim=(2, 3)).add(eps).sqrt()
43
+ return std.expand(x.shape)
44
+ # instance_std = instance_std_tpu
45
+
46
+
47
+ def instance_rms(x, eps: float = 1e-5):
48
+ rms = x.float().square().mean(dim=(2, 3), keepdim=True).add(eps).sqrt().to(x.dtype)
49
+ return rms.expand(x.shape)
50
+
51
+
52
+ def manual_var(x, dim: Union[int, Sequence[int]], diff_sqm: bool = False):
53
+ xm = x.mean(dim=dim, keepdim=True)
54
+ if diff_sqm:
55
+ # difference of squared mean and mean squared, faster on TPU can be less stable
56
+ var = ((x * x).mean(dim=dim, keepdim=True) - (xm * xm)).clamp(0)
57
+ else:
58
+ var = ((x - xm) * (x - xm)).mean(dim=dim, keepdim=True)
59
+ return var
60
+
61
+
62
+ def group_std(x, groups: int = 32, eps: float = 1e-5, flatten: bool = False):
63
+ B, C, H, W = x.shape
64
+ x_dtype = x.dtype
65
+ _assert(C % groups == 0, '')
66
+ if flatten:
67
+ x = x.reshape(B, groups, -1) # FIXME simpler shape causing TPU / XLA issues
68
+ std = x.float().var(dim=2, unbiased=False, keepdim=True).add(eps).sqrt().to(x_dtype)
69
+ else:
70
+ x = x.reshape(B, groups, C // groups, H, W)
71
+ std = x.float().var(dim=(2, 3, 4), unbiased=False, keepdim=True).add(eps).sqrt().to(x_dtype)
72
+ return std.expand(x.shape).reshape(B, C, H, W)
73
+
74
+
75
+ def group_std_tpu(x, groups: int = 32, eps: float = 1e-5, diff_sqm: bool = False, flatten: bool = False):
76
+ # This is a workaround for some stability / odd behaviour of .var and .std
77
+ # running on PyTorch XLA w/ TPUs. These manual var impl are producing much better results
78
+ B, C, H, W = x.shape
79
+ _assert(C % groups == 0, '')
80
+ if flatten:
81
+ x = x.reshape(B, groups, -1) # FIXME simpler shape causing TPU / XLA issues
82
+ var = manual_var(x, dim=-1, diff_sqm=diff_sqm)
83
+ else:
84
+ x = x.reshape(B, groups, C // groups, H, W)
85
+ var = manual_var(x, dim=(2, 3, 4), diff_sqm=diff_sqm)
86
+ return var.add(eps).sqrt().expand(x.shape).reshape(B, C, H, W)
87
+ #group_std = group_std_tpu # FIXME TPU temporary
88
+
89
+
90
+ def group_rms(x, groups: int = 32, eps: float = 1e-5):
91
+ B, C, H, W = x.shape
92
+ _assert(C % groups == 0, '')
93
+ x_dtype = x.dtype
94
+ x = x.reshape(B, groups, C // groups, H, W)
95
+ rms = x.float().square().mean(dim=(2, 3, 4), keepdim=True).add(eps).sqrt_().to(x_dtype)
96
+ return rms.expand(x.shape).reshape(B, C, H, W)
97
+
98
+
99
+ class EvoNorm2dB0(nn.Module):
100
+ def __init__(self, num_features, apply_act=True, momentum=0.1, eps=1e-3, **_):
101
+ super().__init__()
102
+ self.apply_act = apply_act # apply activation (non-linearity)
103
+ self.momentum = momentum
104
+ self.eps = eps
105
+ self.weight = nn.Parameter(torch.ones(num_features))
106
+ self.bias = nn.Parameter(torch.zeros(num_features))
107
+ self.v = nn.Parameter(torch.ones(num_features)) if apply_act else None
108
+ self.register_buffer('running_var', torch.ones(num_features))
109
+ self.reset_parameters()
110
+
111
+ def reset_parameters(self):
112
+ nn.init.ones_(self.weight)
113
+ nn.init.zeros_(self.bias)
114
+ if self.v is not None:
115
+ nn.init.ones_(self.v)
116
+
117
+ def forward(self, x):
118
+ _assert(x.dim() == 4, 'expected 4D input')
119
+ x_dtype = x.dtype
120
+ v_shape = (1, -1, 1, 1)
121
+ if self.v is not None:
122
+ if self.training:
123
+ var = x.float().var(dim=(0, 2, 3), unbiased=False)
124
+ # var = manual_var(x, dim=(0, 2, 3)).squeeze()
125
+ n = x.numel() / x.shape[1]
126
+ self.running_var.copy_(
127
+ self.running_var * (1 - self.momentum) +
128
+ var.detach() * self.momentum * (n / (n - 1)))
129
+ else:
130
+ var = self.running_var
131
+ left = var.add(self.eps).sqrt_().to(x_dtype).view(v_shape).expand_as(x)
132
+ v = self.v.to(x_dtype).view(v_shape)
133
+ right = x * v + instance_std(x, self.eps)
134
+ x = x / left.max(right)
135
+ return x * self.weight.to(x_dtype).view(v_shape) + self.bias.to(x_dtype).view(v_shape)
136
+
137
+
138
+ class EvoNorm2dB1(nn.Module):
139
+ def __init__(self, num_features, apply_act=True, momentum=0.1, eps=1e-5, **_):
140
+ super().__init__()
141
+ self.apply_act = apply_act # apply activation (non-linearity)
142
+ self.momentum = momentum
143
+ self.eps = eps
144
+ self.weight = nn.Parameter(torch.ones(num_features))
145
+ self.bias = nn.Parameter(torch.zeros(num_features))
146
+ self.register_buffer('running_var', torch.ones(num_features))
147
+ self.reset_parameters()
148
+
149
+ def reset_parameters(self):
150
+ nn.init.ones_(self.weight)
151
+ nn.init.zeros_(self.bias)
152
+
153
+ def forward(self, x):
154
+ _assert(x.dim() == 4, 'expected 4D input')
155
+ x_dtype = x.dtype
156
+ v_shape = (1, -1, 1, 1)
157
+ if self.apply_act:
158
+ if self.training:
159
+ var = x.float().var(dim=(0, 2, 3), unbiased=False)
160
+ n = x.numel() / x.shape[1]
161
+ self.running_var.copy_(
162
+ self.running_var * (1 - self.momentum) +
163
+ var.detach().to(self.running_var.dtype) * self.momentum * (n / (n - 1)))
164
+ else:
165
+ var = self.running_var
166
+ var = var.to(x_dtype).view(v_shape)
167
+ left = var.add(self.eps).sqrt_()
168
+ right = (x + 1) * instance_rms(x, self.eps)
169
+ x = x / left.max(right)
170
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
171
+
172
+
173
+ class EvoNorm2dB2(nn.Module):
174
+ def __init__(self, num_features, apply_act=True, momentum=0.1, eps=1e-5, **_):
175
+ super().__init__()
176
+ self.apply_act = apply_act # apply activation (non-linearity)
177
+ self.momentum = momentum
178
+ self.eps = eps
179
+ self.weight = nn.Parameter(torch.ones(num_features))
180
+ self.bias = nn.Parameter(torch.zeros(num_features))
181
+ self.register_buffer('running_var', torch.ones(num_features))
182
+ self.reset_parameters()
183
+
184
+ def reset_parameters(self):
185
+ nn.init.ones_(self.weight)
186
+ nn.init.zeros_(self.bias)
187
+
188
+ def forward(self, x):
189
+ _assert(x.dim() == 4, 'expected 4D input')
190
+ x_dtype = x.dtype
191
+ v_shape = (1, -1, 1, 1)
192
+ if self.apply_act:
193
+ if self.training:
194
+ var = x.float().var(dim=(0, 2, 3), unbiased=False)
195
+ n = x.numel() / x.shape[1]
196
+ self.running_var.copy_(
197
+ self.running_var * (1 - self.momentum) +
198
+ var.detach().to(self.running_var.dtype) * self.momentum * (n / (n - 1)))
199
+ else:
200
+ var = self.running_var
201
+ var = var.to(x_dtype).view(v_shape)
202
+ left = var.add(self.eps).sqrt_()
203
+ right = instance_rms(x, self.eps) - x
204
+ x = x / left.max(right)
205
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
206
+
207
+
208
+ class EvoNorm2dS0(nn.Module):
209
+ def __init__(self, num_features, groups=32, group_size=None, apply_act=True, eps=1e-5, **_):
210
+ super().__init__()
211
+ self.apply_act = apply_act # apply activation (non-linearity)
212
+ if group_size:
213
+ assert num_features % group_size == 0
214
+ self.groups = num_features // group_size
215
+ else:
216
+ self.groups = groups
217
+ self.eps = eps
218
+ self.weight = nn.Parameter(torch.ones(num_features))
219
+ self.bias = nn.Parameter(torch.zeros(num_features))
220
+ self.v = nn.Parameter(torch.ones(num_features)) if apply_act else None
221
+ self.reset_parameters()
222
+
223
+ def reset_parameters(self):
224
+ nn.init.ones_(self.weight)
225
+ nn.init.zeros_(self.bias)
226
+ if self.v is not None:
227
+ nn.init.ones_(self.v)
228
+
229
+ def forward(self, x):
230
+ _assert(x.dim() == 4, 'expected 4D input')
231
+ x_dtype = x.dtype
232
+ v_shape = (1, -1, 1, 1)
233
+ if self.v is not None:
234
+ v = self.v.view(v_shape).to(x_dtype)
235
+ x = x * (x * v).sigmoid() / group_std(x, self.groups, self.eps)
236
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
237
+
238
+
239
+ class EvoNorm2dS0a(EvoNorm2dS0):
240
+ def __init__(self, num_features, groups=32, group_size=None, apply_act=True, eps=1e-3, **_):
241
+ super().__init__(
242
+ num_features, groups=groups, group_size=group_size, apply_act=apply_act, eps=eps)
243
+
244
+ def forward(self, x):
245
+ _assert(x.dim() == 4, 'expected 4D input')
246
+ x_dtype = x.dtype
247
+ v_shape = (1, -1, 1, 1)
248
+ d = group_std(x, self.groups, self.eps)
249
+ if self.v is not None:
250
+ v = self.v.view(v_shape).to(x_dtype)
251
+ x = x * (x * v).sigmoid()
252
+ x = x / d
253
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
254
+
255
+
256
+ class EvoNorm2dS1(nn.Module):
257
+ def __init__(
258
+ self, num_features, groups=32, group_size=None,
259
+ apply_act=True, act_layer=None, eps=1e-5, **_):
260
+ super().__init__()
261
+ act_layer = act_layer or nn.SiLU
262
+ self.apply_act = apply_act # apply activation (non-linearity)
263
+ if act_layer is not None and apply_act:
264
+ self.act = create_act_layer(act_layer)
265
+ else:
266
+ self.act = nn.Identity()
267
+ if group_size:
268
+ assert num_features % group_size == 0
269
+ self.groups = num_features // group_size
270
+ else:
271
+ self.groups = groups
272
+ self.eps = eps
273
+ self.pre_act_norm = False
274
+ self.weight = nn.Parameter(torch.ones(num_features))
275
+ self.bias = nn.Parameter(torch.zeros(num_features))
276
+ self.reset_parameters()
277
+
278
+ def reset_parameters(self):
279
+ nn.init.ones_(self.weight)
280
+ nn.init.zeros_(self.bias)
281
+
282
+ def forward(self, x):
283
+ _assert(x.dim() == 4, 'expected 4D input')
284
+ x_dtype = x.dtype
285
+ v_shape = (1, -1, 1, 1)
286
+ if self.apply_act:
287
+ x = self.act(x) / group_std(x, self.groups, self.eps)
288
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
289
+
290
+
291
+ class EvoNorm2dS1a(EvoNorm2dS1):
292
+ def __init__(
293
+ self, num_features, groups=32, group_size=None,
294
+ apply_act=True, act_layer=None, eps=1e-3, **_):
295
+ super().__init__(
296
+ num_features, groups=groups, group_size=group_size, apply_act=apply_act, act_layer=act_layer, eps=eps)
297
+
298
+ def forward(self, x):
299
+ _assert(x.dim() == 4, 'expected 4D input')
300
+ x_dtype = x.dtype
301
+ v_shape = (1, -1, 1, 1)
302
+ x = self.act(x) / group_std(x, self.groups, self.eps)
303
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
304
+
305
+
306
+ class EvoNorm2dS2(nn.Module):
307
+ def __init__(
308
+ self, num_features, groups=32, group_size=None,
309
+ apply_act=True, act_layer=None, eps=1e-5, **_):
310
+ super().__init__()
311
+ act_layer = act_layer or nn.SiLU
312
+ self.apply_act = apply_act # apply activation (non-linearity)
313
+ if act_layer is not None and apply_act:
314
+ self.act = create_act_layer(act_layer)
315
+ else:
316
+ self.act = nn.Identity()
317
+ if group_size:
318
+ assert num_features % group_size == 0
319
+ self.groups = num_features // group_size
320
+ else:
321
+ self.groups = groups
322
+ self.eps = eps
323
+ self.weight = nn.Parameter(torch.ones(num_features))
324
+ self.bias = nn.Parameter(torch.zeros(num_features))
325
+ self.reset_parameters()
326
+
327
+ def reset_parameters(self):
328
+ nn.init.ones_(self.weight)
329
+ nn.init.zeros_(self.bias)
330
+
331
+ def forward(self, x):
332
+ _assert(x.dim() == 4, 'expected 4D input')
333
+ x_dtype = x.dtype
334
+ v_shape = (1, -1, 1, 1)
335
+ if self.apply_act:
336
+ x = self.act(x) / group_rms(x, self.groups, self.eps)
337
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
338
+
339
+
340
+ class EvoNorm2dS2a(EvoNorm2dS2):
341
+ def __init__(
342
+ self, num_features, groups=32, group_size=None,
343
+ apply_act=True, act_layer=None, eps=1e-3, **_):
344
+ super().__init__(
345
+ num_features, groups=groups, group_size=group_size, apply_act=apply_act, act_layer=act_layer, eps=eps)
346
+
347
+ def forward(self, x):
348
+ _assert(x.dim() == 4, 'expected 4D input')
349
+ x_dtype = x.dtype
350
+ v_shape = (1, -1, 1, 1)
351
+ x = self.act(x) / group_rms(x, self.groups, self.eps)
352
+ return x * self.weight.view(v_shape).to(x_dtype) + self.bias.view(v_shape).to(x_dtype)
reference_medclipseg/biomedclip/layers/fast_norm.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ 'Fast' Normalization Functions
2
+
3
+ For GroupNorm and LayerNorm these functions bypass typical AMP upcast to float32.
4
+
5
+ Additionally, for LayerNorm, the APEX fused LN is used if available (which also does not upcast)
6
+
7
+ Hacked together by / Copyright 2022 Ross Wightman
8
+ """
9
+ from typing import List, Optional
10
+
11
+ import torch
12
+ from torch.nn import functional as F
13
+
14
+ try:
15
+ from apex.normalization.fused_layer_norm import fused_layer_norm_affine
16
+ has_apex = True
17
+ except ImportError:
18
+ has_apex = False
19
+
20
+ try:
21
+ from apex.normalization.fused_layer_norm import fused_rms_norm_affine, fused_rms_norm
22
+ has_apex_rmsnorm = True
23
+ except ImportError:
24
+ has_apex_rmsnorm = False
25
+
26
+
27
+ has_torch_rms_norm = hasattr(F, 'rms_norm')
28
+
29
+ # fast (ie lower precision LN) can be disabled with this flag if issues crop up
30
+ _USE_FAST_NORM = False # defaulting to False for now
31
+
32
+
33
+ def get_autocast_dtype(device: str = 'cuda'):
34
+ try:
35
+ return torch.get_autocast_dtype(device)
36
+ except (AttributeError, TypeError):
37
+ # dispatch to older device specific fns, only covering cuda/cpu devices here
38
+ if device == 'cpu':
39
+ return torch.get_autocast_cpu_dtype()
40
+ else:
41
+ assert device == 'cuda'
42
+ return torch.get_autocast_gpu_dtype()
43
+
44
+
45
+ def is_autocast_enabled(device: str = 'cuda'):
46
+ try:
47
+ return torch.is_autocast_enabled(device)
48
+ except TypeError:
49
+ # dispatch to older device specific fns, only covering cuda/cpu devices here
50
+ if device == 'cpu':
51
+ return torch.is_autocast_cpu_enabled()
52
+ else:
53
+ assert device == 'cuda'
54
+ return torch.is_autocast_enabled() # defaults cuda (only cuda on older pytorch)
55
+
56
+
57
+ def is_fast_norm():
58
+ return _USE_FAST_NORM
59
+
60
+
61
+ def set_fast_norm(enable=True):
62
+ global _USE_FAST_NORM
63
+ _USE_FAST_NORM = enable
64
+
65
+
66
+ def fast_group_norm(
67
+ x: torch.Tensor,
68
+ num_groups: int,
69
+ weight: Optional[torch.Tensor] = None,
70
+ bias: Optional[torch.Tensor] = None,
71
+ eps: float = 1e-5
72
+ ) -> torch.Tensor:
73
+ if torch.jit.is_scripting():
74
+ # currently cannot use is_autocast_enabled within torchscript
75
+ return F.group_norm(x, num_groups, weight, bias, eps)
76
+
77
+ if is_autocast_enabled(x.device.type):
78
+ # normally native AMP casts GN inputs to float32
79
+ # here we use the low precision autocast dtype
80
+ dt = get_autocast_dtype(x.device.type)
81
+ x, weight, bias = x.to(dt), weight.to(dt), bias.to(dt) if bias is not None else None
82
+
83
+ with torch.amp.autocast(device_type=x.device.type, enabled=False):
84
+ return F.group_norm(x, num_groups, weight, bias, eps)
85
+
86
+
87
+ def fast_layer_norm(
88
+ x: torch.Tensor,
89
+ normalized_shape: List[int],
90
+ weight: Optional[torch.Tensor] = None,
91
+ bias: Optional[torch.Tensor] = None,
92
+ eps: float = 1e-5
93
+ ) -> torch.Tensor:
94
+ if torch.jit.is_scripting():
95
+ # currently cannot use is_autocast_enabled within torchscript
96
+ return F.layer_norm(x, normalized_shape, weight, bias, eps)
97
+
98
+ if has_apex:
99
+ return fused_layer_norm_affine(x, weight, bias, normalized_shape, eps)
100
+
101
+ if is_autocast_enabled(x.device.type):
102
+ # normally native AMP casts LN inputs to float32
103
+ # apex LN does not, this is behaving like Apex
104
+ dt = get_autocast_dtype(x.device.type)
105
+ x, weight, bias = x.to(dt), weight.to(dt), bias.to(dt) if bias is not None else None
106
+
107
+ with torch.amp.autocast(device_type=x.device.type, enabled=False):
108
+ return F.layer_norm(x, normalized_shape, weight, bias, eps)
109
+
110
+
111
+ def rms_norm(
112
+ x: torch.Tensor,
113
+ normalized_shape: List[int],
114
+ weight: Optional[torch.Tensor] = None,
115
+ eps: float = 1e-5,
116
+ ):
117
+ norm_ndim = len(normalized_shape)
118
+ v = x.pow(2)
119
+ if torch.jit.is_scripting():
120
+ # ndim = len(x.shape)
121
+ # dims = list(range(ndim - norm_ndim, ndim)) # this doesn't work on pytorch <= 1.13.x
122
+ # NOTE -ve dims cause torchscript to crash in some cases, out of options to work around
123
+ assert norm_ndim == 1
124
+ v = torch.mean(v, dim=-1).unsqueeze(-1) # ts crashes with -ve dim + keepdim=True
125
+ else:
126
+ dims = tuple(range(-1, -norm_ndim - 1, -1))
127
+ v = torch.mean(v, dim=dims, keepdim=True)
128
+ x = x * torch.rsqrt(v + eps)
129
+ if weight is not None:
130
+ x = x * weight
131
+ return x
132
+
133
+
134
+ def fast_rms_norm(
135
+ x: torch.Tensor,
136
+ normalized_shape: List[int],
137
+ weight: Optional[torch.Tensor] = None,
138
+ eps: float = 1e-5,
139
+ ) -> torch.Tensor:
140
+ if torch.jit.is_scripting():
141
+ # this must be by itself, cannot merge with has_apex_rmsnorm
142
+ return rms_norm(x, normalized_shape, weight, eps)
143
+
144
+ if has_apex_rmsnorm:
145
+ if weight is None:
146
+ return fused_rms_norm(x, normalized_shape, eps)
147
+ else:
148
+ return fused_rms_norm_affine(x, weight, normalized_shape, eps)
149
+
150
+ if is_autocast_enabled(x.device.type):
151
+ # normally native AMP casts LN inputs to float32
152
+ # apex LN does not, this is behaving like Apex
153
+ dt = get_autocast_dtype(x.device.type)
154
+ x, weight = x.to(dt), weight.to(dt)
155
+
156
+ with torch.amp.autocast(device_type=x.device.type, enabled=False):
157
+ if has_torch_rms_norm:
158
+ x = F.rms_norm(x, normalized_shape, weight, eps)
159
+ else:
160
+ x = rms_norm(x, normalized_shape, weight, eps)
161
+
162
+ return x
163
+
164
+
165
+ def simple_norm(
166
+ x: torch.Tensor,
167
+ normalized_shape: List[int],
168
+ weight: Optional[torch.Tensor] = None,
169
+ eps: float = 1e-5,
170
+ ):
171
+ norm_ndim = len(normalized_shape)
172
+ if torch.jit.is_scripting():
173
+ # ndim = len(x.shape)
174
+ # dims = list(range(ndim - norm_ndim, ndim)) # this doesn't work on pytorch <= 1.13.x
175
+ # NOTE -ve dims cause torchscript to crash in some cases, out of options to work around
176
+ assert norm_ndim == 1
177
+ v = torch.var(x, dim=-1).unsqueeze(-1) # ts crashes with -ve dim + keepdim=True
178
+ else:
179
+ dims = tuple(range(-1, -norm_ndim - 1, -1))
180
+ v = torch.var(x, dim=dims, keepdim=True)
181
+ x = x * torch.rsqrt(v + eps)
182
+ if weight is not None:
183
+ x = x * weight
184
+ return x
185
+
186
+
187
+ def fast_simple_norm(
188
+ x: torch.Tensor,
189
+ normalized_shape: List[int],
190
+ weight: Optional[torch.Tensor] = None,
191
+ eps: float = 1e-5,
192
+ ) -> torch.Tensor:
193
+ if torch.jit.is_scripting():
194
+ # this must be by itself, cannot merge with has_apex_rmsnorm
195
+ return simple_norm(x, normalized_shape, weight, eps)
196
+
197
+ if is_autocast_enabled(x.device.type):
198
+ # normally native AMP casts LN inputs to float32
199
+ # apex LN does not, this is behaving like Apex
200
+ dt = get_autocast_dtype(x.device.type)
201
+ x, weight = x.to(dt), weight.to(dt)
202
+
203
+ with torch.amp.autocast(device_type=x.device.type, enabled=False):
204
+ x = simple_norm(x, normalized_shape, weight, eps)
205
+ return x
206
+
reference_medclipseg/biomedclip/layers/filter_response_norm.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Filter Response Norm in PyTorch
2
+
3
+ Based on `Filter Response Normalization Layer` - https://arxiv.org/abs/1911.09737
4
+
5
+ Hacked together by / Copyright 2021 Ross Wightman
6
+ """
7
+ import torch
8
+ import torch.nn as nn
9
+
10
+ from .create_act import create_act_layer
11
+ from .trace_utils import _assert
12
+
13
+
14
+ def inv_instance_rms(x, eps: float = 1e-5):
15
+ rms = x.square().float().mean(dim=(2, 3), keepdim=True).add(eps).rsqrt().to(x.dtype)
16
+ return rms.expand(x.shape)
17
+
18
+
19
+ class FilterResponseNormTlu2d(nn.Module):
20
+ def __init__(self, num_features, apply_act=True, eps=1e-5, rms=True, **_):
21
+ super(FilterResponseNormTlu2d, self).__init__()
22
+ self.apply_act = apply_act # apply activation (non-linearity)
23
+ self.rms = rms
24
+ self.eps = eps
25
+ self.weight = nn.Parameter(torch.ones(num_features))
26
+ self.bias = nn.Parameter(torch.zeros(num_features))
27
+ self.tau = nn.Parameter(torch.zeros(num_features)) if apply_act else None
28
+ self.reset_parameters()
29
+
30
+ def reset_parameters(self):
31
+ nn.init.ones_(self.weight)
32
+ nn.init.zeros_(self.bias)
33
+ if self.tau is not None:
34
+ nn.init.zeros_(self.tau)
35
+
36
+ def forward(self, x):
37
+ _assert(x.dim() == 4, 'expected 4D input')
38
+ x_dtype = x.dtype
39
+ v_shape = (1, -1, 1, 1)
40
+ x = x * inv_instance_rms(x, self.eps)
41
+ x = x * self.weight.view(v_shape).to(dtype=x_dtype) + self.bias.view(v_shape).to(dtype=x_dtype)
42
+ return torch.maximum(x, self.tau.reshape(v_shape).to(dtype=x_dtype)) if self.tau is not None else x
43
+
44
+
45
+ class FilterResponseNormAct2d(nn.Module):
46
+ def __init__(self, num_features, apply_act=True, act_layer=nn.ReLU, inplace=None, rms=True, eps=1e-5, **_):
47
+ super(FilterResponseNormAct2d, self).__init__()
48
+ if act_layer is not None and apply_act:
49
+ self.act = create_act_layer(act_layer, inplace=inplace)
50
+ else:
51
+ self.act = nn.Identity()
52
+ self.rms = rms
53
+ self.eps = eps
54
+ self.weight = nn.Parameter(torch.ones(num_features))
55
+ self.bias = nn.Parameter(torch.zeros(num_features))
56
+ self.reset_parameters()
57
+
58
+ def reset_parameters(self):
59
+ nn.init.ones_(self.weight)
60
+ nn.init.zeros_(self.bias)
61
+
62
+ def forward(self, x):
63
+ _assert(x.dim() == 4, 'expected 4D input')
64
+ x_dtype = x.dtype
65
+ v_shape = (1, -1, 1, 1)
66
+ x = x * inv_instance_rms(x, self.eps)
67
+ x = x * self.weight.view(v_shape).to(dtype=x_dtype) + self.bias.view(v_shape).to(dtype=x_dtype)
68
+ return self.act(x)
reference_medclipseg/biomedclip/layers/format.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from typing import Union
3
+
4
+ import torch
5
+
6
+
7
+ class Format(str, Enum):
8
+ NCHW = 'NCHW'
9
+ NHWC = 'NHWC'
10
+ NCL = 'NCL'
11
+ NLC = 'NLC'
12
+
13
+
14
+ FormatT = Union[str, Format]
15
+
16
+
17
+ def get_spatial_dim(fmt: FormatT):
18
+ fmt = Format(fmt)
19
+ if fmt is Format.NLC:
20
+ dim = (1,)
21
+ elif fmt is Format.NCL:
22
+ dim = (2,)
23
+ elif fmt is Format.NHWC:
24
+ dim = (1, 2)
25
+ else:
26
+ dim = (2, 3)
27
+ return dim
28
+
29
+
30
+ def get_channel_dim(fmt: FormatT):
31
+ fmt = Format(fmt)
32
+ if fmt is Format.NHWC:
33
+ dim = 3
34
+ elif fmt is Format.NLC:
35
+ dim = 2
36
+ else:
37
+ dim = 1
38
+ return dim
39
+
40
+
41
+ def nchw_to(x: torch.Tensor, fmt: Format):
42
+ if fmt == Format.NHWC:
43
+ x = x.permute(0, 2, 3, 1)
44
+ elif fmt == Format.NLC:
45
+ x = x.flatten(2).transpose(1, 2)
46
+ elif fmt == Format.NCL:
47
+ x = x.flatten(2)
48
+ return x
49
+
50
+
51
+ def nhwc_to(x: torch.Tensor, fmt: Format):
52
+ if fmt == Format.NCHW:
53
+ x = x.permute(0, 3, 1, 2)
54
+ elif fmt == Format.NLC:
55
+ x = x.flatten(1, 2)
56
+ elif fmt == Format.NCL:
57
+ x = x.flatten(1, 2).transpose(1, 2)
58
+ return x
reference_medclipseg/biomedclip/layers/gather_excite.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Gather-Excite Attention Block
2
+
3
+ Paper: `Gather-Excite: Exploiting Feature Context in CNNs` - https://arxiv.org/abs/1810.12348
4
+
5
+ Official code here, but it's only partial impl in Caffe: https://github.com/hujie-frank/GENet
6
+
7
+ I've tried to support all of the extent both w/ and w/o params. I don't believe I've seen another
8
+ impl that covers all of the cases.
9
+
10
+ NOTE: extent=0 + extra_params=False is equivalent to Squeeze-and-Excitation
11
+
12
+ Hacked together by / Copyright 2021 Ross Wightman
13
+ """
14
+ import math
15
+
16
+ from torch import nn as nn
17
+ import torch.nn.functional as F
18
+
19
+ from .create_act import create_act_layer, get_act_layer
20
+ from .create_conv2d import create_conv2d
21
+ from .helpers import make_divisible
22
+ from .mlp import ConvMlp
23
+
24
+
25
+ class GatherExcite(nn.Module):
26
+ """ Gather-Excite Attention Module
27
+ """
28
+ def __init__(
29
+ self, channels, feat_size=None, extra_params=False, extent=0, use_mlp=True,
30
+ rd_ratio=1./16, rd_channels=None, rd_divisor=1, add_maxpool=False,
31
+ act_layer=nn.ReLU, norm_layer=nn.BatchNorm2d, gate_layer='sigmoid'):
32
+ super(GatherExcite, self).__init__()
33
+ self.add_maxpool = add_maxpool
34
+ act_layer = get_act_layer(act_layer)
35
+ self.extent = extent
36
+ if extra_params:
37
+ self.gather = nn.Sequential()
38
+ if extent == 0:
39
+ assert feat_size is not None, 'spatial feature size must be specified for global extent w/ params'
40
+ self.gather.add_module(
41
+ 'conv1', create_conv2d(channels, channels, kernel_size=feat_size, stride=1, depthwise=True))
42
+ if norm_layer:
43
+ self.gather.add_module(f'norm1', nn.BatchNorm2d(channels))
44
+ else:
45
+ assert extent % 2 == 0
46
+ num_conv = int(math.log2(extent))
47
+ for i in range(num_conv):
48
+ self.gather.add_module(
49
+ f'conv{i + 1}',
50
+ create_conv2d(channels, channels, kernel_size=3, stride=2, depthwise=True))
51
+ if norm_layer:
52
+ self.gather.add_module(f'norm{i + 1}', nn.BatchNorm2d(channels))
53
+ if i != num_conv - 1:
54
+ self.gather.add_module(f'act{i + 1}', act_layer(inplace=True))
55
+ else:
56
+ self.gather = None
57
+ if self.extent == 0:
58
+ self.gk = 0
59
+ self.gs = 0
60
+ else:
61
+ assert extent % 2 == 0
62
+ self.gk = self.extent * 2 - 1
63
+ self.gs = self.extent
64
+
65
+ if not rd_channels:
66
+ rd_channels = make_divisible(channels * rd_ratio, rd_divisor, round_limit=0.)
67
+ self.mlp = ConvMlp(channels, rd_channels, act_layer=act_layer) if use_mlp else nn.Identity()
68
+ self.gate = create_act_layer(gate_layer)
69
+
70
+ def forward(self, x):
71
+ size = x.shape[-2:]
72
+ if self.gather is not None:
73
+ x_ge = self.gather(x)
74
+ else:
75
+ if self.extent == 0:
76
+ # global extent
77
+ x_ge = x.mean(dim=(2, 3), keepdims=True)
78
+ if self.add_maxpool:
79
+ # experimental codepath, may remove or change
80
+ x_ge = 0.5 * x_ge + 0.5 * x.amax((2, 3), keepdim=True)
81
+ else:
82
+ x_ge = F.avg_pool2d(
83
+ x, kernel_size=self.gk, stride=self.gs, padding=self.gk // 2, count_include_pad=False)
84
+ if self.add_maxpool:
85
+ # experimental codepath, may remove or change
86
+ x_ge = 0.5 * x_ge + 0.5 * F.max_pool2d(x, kernel_size=self.gk, stride=self.gs, padding=self.gk // 2)
87
+ x_ge = self.mlp(x_ge)
88
+ if x_ge.shape[-1] != 1 or x_ge.shape[-2] != 1:
89
+ x_ge = F.interpolate(x_ge, size=size)
90
+ return x * self.gate(x_ge)
reference_medclipseg/biomedclip/layers/global_context.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Global Context Attention Block
2
+
3
+ Paper: `GCNet: Non-local Networks Meet Squeeze-Excitation Networks and Beyond`
4
+ - https://arxiv.org/abs/1904.11492
5
+
6
+ Official code consulted as reference: https://github.com/xvjiarui/GCNet
7
+
8
+ Hacked together by / Copyright 2021 Ross Wightman
9
+ """
10
+ from torch import nn as nn
11
+ import torch.nn.functional as F
12
+
13
+ from .create_act import create_act_layer, get_act_layer
14
+ from .helpers import make_divisible
15
+ from .mlp import ConvMlp
16
+ from .norm import LayerNorm2d
17
+
18
+
19
+ class GlobalContext(nn.Module):
20
+
21
+ def __init__(self, channels, use_attn=True, fuse_add=False, fuse_scale=True, init_last_zero=False,
22
+ rd_ratio=1./8, rd_channels=None, rd_divisor=1, act_layer=nn.ReLU, gate_layer='sigmoid'):
23
+ super(GlobalContext, self).__init__()
24
+ act_layer = get_act_layer(act_layer)
25
+
26
+ self.conv_attn = nn.Conv2d(channels, 1, kernel_size=1, bias=True) if use_attn else None
27
+
28
+ if rd_channels is None:
29
+ rd_channels = make_divisible(channels * rd_ratio, rd_divisor, round_limit=0.)
30
+ if fuse_add:
31
+ self.mlp_add = ConvMlp(channels, rd_channels, act_layer=act_layer, norm_layer=LayerNorm2d)
32
+ else:
33
+ self.mlp_add = None
34
+ if fuse_scale:
35
+ self.mlp_scale = ConvMlp(channels, rd_channels, act_layer=act_layer, norm_layer=LayerNorm2d)
36
+ else:
37
+ self.mlp_scale = None
38
+
39
+ self.gate = create_act_layer(gate_layer)
40
+ self.init_last_zero = init_last_zero
41
+ self.reset_parameters()
42
+
43
+ def reset_parameters(self):
44
+ if self.conv_attn is not None:
45
+ nn.init.kaiming_normal_(self.conv_attn.weight, mode='fan_in', nonlinearity='relu')
46
+ if self.mlp_add is not None:
47
+ nn.init.zeros_(self.mlp_add.fc2.weight)
48
+
49
+ def forward(self, x):
50
+ B, C, H, W = x.shape
51
+
52
+ if self.conv_attn is not None:
53
+ attn = self.conv_attn(x).reshape(B, 1, H * W) # (B, 1, H * W)
54
+ attn = F.softmax(attn, dim=-1).unsqueeze(3) # (B, 1, H * W, 1)
55
+ context = x.reshape(B, C, H * W).unsqueeze(1) @ attn
56
+ context = context.view(B, C, 1, 1)
57
+ else:
58
+ context = x.mean(dim=(2, 3), keepdim=True)
59
+
60
+ if self.mlp_scale is not None:
61
+ mlp_x = self.mlp_scale(context)
62
+ x = x * self.gate(mlp_x)
63
+ if self.mlp_add is not None:
64
+ mlp_x = self.mlp_add(context)
65
+ x = x + mlp_x
66
+
67
+ return x
reference_medclipseg/biomedclip/layers/grid.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple
2
+
3
+ import torch
4
+
5
+
6
+ def ndgrid(*tensors) -> Tuple[torch.Tensor, ...]:
7
+ """generate N-D grid in dimension order.
8
+
9
+ The ndgrid function is like meshgrid except that the order of the first two input arguments are switched.
10
+
11
+ That is, the statement
12
+ [X1,X2,X3] = ndgrid(x1,x2,x3)
13
+
14
+ produces the same result as
15
+
16
+ [X2,X1,X3] = meshgrid(x2,x1,x3)
17
+
18
+ This naming is based on MATLAB, the purpose is to avoid confusion due to torch's change to make
19
+ torch.meshgrid behaviour move from matching ndgrid ('ij') indexing to numpy meshgrid defaults of ('xy').
20
+
21
+ """
22
+ try:
23
+ return torch.meshgrid(*tensors, indexing='ij')
24
+ except TypeError:
25
+ # old PyTorch < 1.10 will follow this path as it does not have indexing arg,
26
+ # the old behaviour of meshgrid was 'ij'
27
+ return torch.meshgrid(*tensors)
28
+
29
+
30
+ def meshgrid(*tensors) -> Tuple[torch.Tensor, ...]:
31
+ """generate N-D grid in spatial dim order.
32
+
33
+ The meshgrid function is similar to ndgrid except that the order of the
34
+ first two input and output arguments is switched.
35
+
36
+ That is, the statement
37
+
38
+ [X,Y,Z] = meshgrid(x,y,z)
39
+ produces the same result as
40
+
41
+ [Y,X,Z] = ndgrid(y,x,z)
42
+ Because of this, meshgrid is better suited to problems in two- or three-dimensional Cartesian space,
43
+ while ndgrid is better suited to multidimensional problems that aren't spatially based.
44
+ """
45
+
46
+ # NOTE: this will throw in PyTorch < 1.10 as meshgrid did not support indexing arg or have
47
+ # capability of generating grid in xy order before then.
48
+ return torch.meshgrid(*tensors, indexing='xy')
49
+
reference_medclipseg/biomedclip/layers/grn.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Global Response Normalization Module
2
+
3
+ Based on the GRN layer presented in
4
+ `ConvNeXt-V2 - Co-designing and Scaling ConvNets with Masked Autoencoders` - https://arxiv.org/abs/2301.00808
5
+
6
+ This implementation
7
+ * works for both NCHW and NHWC tensor layouts
8
+ * uses affine param names matching existing torch norm layers
9
+ * slightly improves eager mode performance via fused addcmul
10
+
11
+ Hacked together by / Copyright 2023 Ross Wightman
12
+ """
13
+
14
+ import torch
15
+ from torch import nn as nn
16
+
17
+
18
+ class GlobalResponseNorm(nn.Module):
19
+ """ Global Response Normalization layer
20
+ """
21
+ def __init__(self, dim, eps=1e-6, channels_last=True):
22
+ super().__init__()
23
+ self.eps = eps
24
+ if channels_last:
25
+ self.spatial_dim = (1, 2)
26
+ self.channel_dim = -1
27
+ self.wb_shape = (1, 1, 1, -1)
28
+ else:
29
+ self.spatial_dim = (2, 3)
30
+ self.channel_dim = 1
31
+ self.wb_shape = (1, -1, 1, 1)
32
+
33
+ self.weight = nn.Parameter(torch.zeros(dim))
34
+ self.bias = nn.Parameter(torch.zeros(dim))
35
+
36
+ def forward(self, x):
37
+ x_g = x.norm(p=2, dim=self.spatial_dim, keepdim=True)
38
+ x_n = x_g / (x_g.mean(dim=self.channel_dim, keepdim=True) + self.eps)
39
+ return x + torch.addcmul(self.bias.view(self.wb_shape), self.weight.view(self.wb_shape), x * x_n)
reference_medclipseg/biomedclip/layers/halo_attn.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Halo Self Attention
2
+
3
+ Paper: `Scaling Local Self-Attention for Parameter Efficient Visual Backbones`
4
+ - https://arxiv.org/abs/2103.12731
5
+
6
+ @misc{2103.12731,
7
+ Author = {Ashish Vaswani and Prajit Ramachandran and Aravind Srinivas and Niki Parmar and Blake Hechtman and
8
+ Jonathon Shlens},
9
+ Title = {Scaling Local Self-Attention for Parameter Efficient Visual Backbones},
10
+ Year = {2021},
11
+ }
12
+
13
+ Status:
14
+ This impl is a WIP, there is no official ref impl and some details in paper weren't clear to me.
15
+ The attention mechanism works but it's slow as implemented.
16
+
17
+ Hacked together by / Copyright 2021 Ross Wightman
18
+ """
19
+ from typing import List
20
+
21
+ import torch
22
+ from torch import nn
23
+ import torch.nn.functional as F
24
+
25
+ from .helpers import make_divisible
26
+ from .weight_init import trunc_normal_
27
+ from .trace_utils import _assert
28
+
29
+
30
+ def rel_logits_1d(q, rel_k, permute_mask: List[int]):
31
+ """ Compute relative logits along one dimension
32
+
33
+ As per: https://gist.github.com/aravindsrinivas/56359b79f0ce4449bcb04ab4b56a57a2
34
+ Originally from: `Attention Augmented Convolutional Networks` - https://arxiv.org/abs/1904.09925
35
+
36
+ Args:
37
+ q: (batch, height, width, dim)
38
+ rel_k: (2 * window - 1, dim)
39
+ permute_mask: permute output dim according to this
40
+ """
41
+ B, H, W, dim = q.shape
42
+ rel_size = rel_k.shape[0]
43
+ win_size = (rel_size + 1) // 2
44
+
45
+ x = (q @ rel_k.transpose(-1, -2))
46
+ x = x.reshape(-1, W, rel_size)
47
+
48
+ # pad to shift from relative to absolute indexing
49
+ x_pad = F.pad(x, [0, 1]).flatten(1)
50
+ x_pad = F.pad(x_pad, [0, rel_size - W])
51
+
52
+ # reshape and slice out the padded elements
53
+ x_pad = x_pad.reshape(-1, W + 1, rel_size)
54
+ x = x_pad[:, :W, win_size - 1:]
55
+
56
+ # reshape and tile
57
+ x = x.reshape(B, H, 1, W, win_size).expand(-1, -1, win_size, -1, -1)
58
+ return x.permute(permute_mask)
59
+
60
+
61
+ class PosEmbedRel(nn.Module):
62
+ """ Relative Position Embedding
63
+ As per: https://gist.github.com/aravindsrinivas/56359b79f0ce4449bcb04ab4b56a57a2
64
+ Originally from: `Attention Augmented Convolutional Networks` - https://arxiv.org/abs/1904.09925
65
+
66
+ """
67
+ def __init__(self, block_size, win_size, dim_head, scale):
68
+ """
69
+ Args:
70
+ block_size (int): block size
71
+ win_size (int): neighbourhood window size
72
+ dim_head (int): attention head dim
73
+ scale (float): scale factor (for init)
74
+ """
75
+ super().__init__()
76
+ self.block_size = block_size
77
+ self.dim_head = dim_head
78
+ self.height_rel = nn.Parameter(torch.randn(win_size * 2 - 1, dim_head) * scale)
79
+ self.width_rel = nn.Parameter(torch.randn(win_size * 2 - 1, dim_head) * scale)
80
+
81
+ def forward(self, q):
82
+ B, BB, HW, _ = q.shape
83
+
84
+ # relative logits in width dimension.
85
+ q = q.reshape(-1, self.block_size, self.block_size, self.dim_head)
86
+ rel_logits_w = rel_logits_1d(q, self.width_rel, permute_mask=(0, 1, 3, 2, 4))
87
+
88
+ # relative logits in height dimension.
89
+ q = q.transpose(1, 2)
90
+ rel_logits_h = rel_logits_1d(q, self.height_rel, permute_mask=(0, 3, 1, 4, 2))
91
+
92
+ rel_logits = rel_logits_h + rel_logits_w
93
+ rel_logits = rel_logits.reshape(B, BB, HW, -1)
94
+ return rel_logits
95
+
96
+
97
+ class HaloAttn(nn.Module):
98
+ """ Halo Attention
99
+
100
+ Paper: `Scaling Local Self-Attention for Parameter Efficient Visual Backbones`
101
+ - https://arxiv.org/abs/2103.12731
102
+
103
+ The internal dimensions of the attention module are controlled by the interaction of several arguments.
104
+ * the output dimension of the module is specified by dim_out, which falls back to input dim if not set
105
+ * the value (v) dimension is set to dim_out // num_heads, the v projection determines the output dim
106
+ * the query and key (qk) dimensions are determined by
107
+ * num_heads * dim_head if dim_head is not None
108
+ * num_heads * (dim_out * attn_ratio // num_heads) if dim_head is None
109
+ * as seen above, attn_ratio determines the ratio of q and k relative to the output if dim_head not used
110
+
111
+ Args:
112
+ dim (int): input dimension to the module
113
+ dim_out (int): output dimension of the module, same as dim if not set
114
+ feat_size (Tuple[int, int]): size of input feature_map (not used, for arg compat with bottle/lambda)
115
+ stride: output stride of the module, query downscaled if > 1 (default: 1).
116
+ num_heads: parallel attention heads (default: 8).
117
+ dim_head: dimension of query and key heads, calculated from dim_out * attn_ratio // num_heads if not set
118
+ block_size (int): size of blocks. (default: 8)
119
+ halo_size (int): size of halo overlap. (default: 3)
120
+ qk_ratio (float): ratio of q and k dimensions to output dimension when dim_head not set. (default: 1.0)
121
+ qkv_bias (bool) : add bias to q, k, and v projections
122
+ avg_down (bool): use average pool downsample instead of strided query blocks
123
+ scale_pos_embed (bool): scale the position embedding as well as Q @ K
124
+ """
125
+ def __init__(
126
+ self, dim, dim_out=None, feat_size=None, stride=1, num_heads=8, dim_head=None, block_size=8, halo_size=3,
127
+ qk_ratio=1.0, qkv_bias=False, avg_down=False, scale_pos_embed=False):
128
+ super().__init__()
129
+ dim_out = dim_out or dim
130
+ assert dim_out % num_heads == 0
131
+ assert stride in (1, 2)
132
+ self.num_heads = num_heads
133
+ self.dim_head_qk = dim_head or make_divisible(dim_out * qk_ratio, divisor=8) // num_heads
134
+ self.dim_head_v = dim_out // self.num_heads
135
+ self.dim_out_qk = num_heads * self.dim_head_qk
136
+ self.dim_out_v = num_heads * self.dim_head_v
137
+ self.scale = self.dim_head_qk ** -0.5
138
+ self.scale_pos_embed = scale_pos_embed
139
+ self.block_size = self.block_size_ds = block_size
140
+ self.halo_size = halo_size
141
+ self.win_size = block_size + halo_size * 2 # neighbourhood window size
142
+ self.block_stride = 1
143
+ use_avg_pool = False
144
+ if stride > 1:
145
+ use_avg_pool = avg_down or block_size % stride != 0
146
+ self.block_stride = 1 if use_avg_pool else stride
147
+ self.block_size_ds = self.block_size // self.block_stride
148
+
149
+ # FIXME not clear if this stride behaviour is what the paper intended
150
+ # Also, the paper mentions using a 3D conv for dealing with the blocking/gather, and leaving
151
+ # data in unfolded block form. I haven't wrapped my head around how that'd look.
152
+ self.q = nn.Conv2d(dim, self.dim_out_qk, 1, stride=self.block_stride, bias=qkv_bias)
153
+ self.kv = nn.Conv2d(dim, self.dim_out_qk + self.dim_out_v, 1, bias=qkv_bias)
154
+
155
+ self.pos_embed = PosEmbedRel(
156
+ block_size=self.block_size_ds, win_size=self.win_size, dim_head=self.dim_head_qk, scale=self.scale)
157
+
158
+ self.pool = nn.AvgPool2d(2, 2) if use_avg_pool else nn.Identity()
159
+
160
+ self.reset_parameters()
161
+
162
+ def reset_parameters(self):
163
+ std = self.q.weight.shape[1] ** -0.5 # fan-in
164
+ trunc_normal_(self.q.weight, std=std)
165
+ trunc_normal_(self.kv.weight, std=std)
166
+ trunc_normal_(self.pos_embed.height_rel, std=self.scale)
167
+ trunc_normal_(self.pos_embed.width_rel, std=self.scale)
168
+
169
+ def forward(self, x):
170
+ B, C, H, W = x.shape
171
+ _assert(H % self.block_size == 0, '')
172
+ _assert(W % self.block_size == 0, '')
173
+ num_h_blocks = H // self.block_size
174
+ num_w_blocks = W // self.block_size
175
+ num_blocks = num_h_blocks * num_w_blocks
176
+
177
+ q = self.q(x)
178
+ # unfold
179
+ q = q.reshape(
180
+ -1, self.dim_head_qk,
181
+ num_h_blocks, self.block_size_ds, num_w_blocks, self.block_size_ds).permute(0, 1, 3, 5, 2, 4)
182
+ # B, num_heads * dim_head * block_size ** 2, num_blocks
183
+ q = q.reshape(B * self.num_heads, self.dim_head_qk, -1, num_blocks).transpose(1, 3)
184
+ # B * num_heads, num_blocks, block_size ** 2, dim_head
185
+
186
+ kv = self.kv(x)
187
+ # Generate overlapping windows for kv. This approach is good for GPU and CPU. However, unfold() is not
188
+ # lowered for PyTorch XLA so it will be very slow. See code at bottom of file for XLA friendly approach.
189
+ # FIXME figure out how to switch impl between this and conv2d if XLA being used.
190
+ kv = F.pad(kv, [self.halo_size, self.halo_size, self.halo_size, self.halo_size])
191
+ kv = kv.unfold(2, self.win_size, self.block_size).unfold(3, self.win_size, self.block_size).reshape(
192
+ B * self.num_heads, self.dim_head_qk + self.dim_head_v, num_blocks, -1).permute(0, 2, 3, 1)
193
+ k, v = torch.split(kv, [self.dim_head_qk, self.dim_head_v], dim=-1)
194
+ # B * num_heads, num_blocks, win_size ** 2, dim_head_qk or dim_head_v
195
+
196
+ if self.scale_pos_embed:
197
+ attn = (q @ k.transpose(-1, -2) + self.pos_embed(q)) * self.scale
198
+ else:
199
+ attn = (q @ k.transpose(-1, -2)) * self.scale + self.pos_embed(q)
200
+ # B * num_heads, num_blocks, block_size ** 2, win_size ** 2
201
+ attn = attn.softmax(dim=-1)
202
+
203
+ out = (attn @ v).transpose(1, 3) # B * num_heads, dim_head_v, block_size ** 2, num_blocks
204
+ # fold
205
+ out = out.reshape(-1, self.block_size_ds, self.block_size_ds, num_h_blocks, num_w_blocks)
206
+ out = out.permute(0, 3, 1, 4, 2).contiguous().view(
207
+ B, self.dim_out_v, H // self.block_stride, W // self.block_stride)
208
+ # B, dim_out, H // block_stride, W // block_stride
209
+ out = self.pool(out)
210
+ return out
211
+
212
+
213
+ """ Three alternatives for overlapping windows.
214
+
215
+ `.unfold().unfold()` is same speed as stride tricks with similar clarity as F.unfold()
216
+
217
+ if is_xla:
218
+ # This code achieves haloing on PyTorch XLA with reasonable runtime trade-off, it is
219
+ # EXTREMELY slow for backward on a GPU though so I need a way of selecting based on environment.
220
+ WW = self.win_size ** 2
221
+ pw = torch.eye(WW, dtype=x.dtype, device=x.device).reshape(WW, 1, self.win_size, self.win_size)
222
+ kv = F.conv2d(kv.reshape(-1, 1, H, W), pw, stride=self.block_size, padding=self.halo_size)
223
+ elif self.stride_tricks:
224
+ kv = F.pad(kv, [self.halo_size, self.halo_size, self.halo_size, self.halo_size]).contiguous()
225
+ kv = kv.as_strided((
226
+ B, self.dim_out_qk + self.dim_out_v, self.win_size, self.win_size, num_h_blocks, num_w_blocks),
227
+ stride=(kv.stride(0), kv.stride(1), kv.shape[-1], 1, self.block_size * kv.shape[-1], self.block_size))
228
+ else:
229
+ kv = F.unfold(kv, kernel_size=self.win_size, stride=self.block_size, padding=self.halo_size)
230
+
231
+ kv = kv.reshape(
232
+ B * self.num_heads, self.dim_head_qk + self.dim_head_v, -1, num_blocks).transpose(1, 3)
233
+ """
reference_medclipseg/biomedclip/layers/helpers.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Layer/Module Helpers
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+ from itertools import repeat
6
+ import collections.abc
7
+
8
+
9
+ # From PyTorch internals
10
+ def _ntuple(n):
11
+ def parse(x):
12
+ if isinstance(x, collections.abc.Iterable) and not isinstance(x, str):
13
+ return tuple(x)
14
+ return tuple(repeat(x, n))
15
+ return parse
16
+
17
+
18
+ to_1tuple = _ntuple(1)
19
+ to_2tuple = _ntuple(2)
20
+ to_3tuple = _ntuple(3)
21
+ to_4tuple = _ntuple(4)
22
+ to_ntuple = _ntuple
23
+
24
+
25
+ def make_divisible(v, divisor=8, min_value=None, round_limit=.9):
26
+ min_value = min_value or divisor
27
+ new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
28
+ # Make sure that round down does not go down by more than 10%.
29
+ if new_v < round_limit * v:
30
+ new_v += divisor
31
+ return new_v
32
+
33
+
34
+ def extend_tuple(x, n):
35
+ # pads a tuple to specified n by padding with last value
36
+ if not isinstance(x, (tuple, list)):
37
+ x = (x,)
38
+ else:
39
+ x = tuple(x)
40
+ pad_n = n - len(x)
41
+ if pad_n <= 0:
42
+ return x[:n]
43
+ return x + (x[-1],) * pad_n
reference_medclipseg/biomedclip/layers/hybrid_embed.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Image to Patch Hybird Embedding Layer
2
+
3
+ Hacked together by / Copyright 2020 Ross Wightman
4
+ """
5
+ import logging
6
+ import math
7
+ from typing import List, Optional, Tuple, Union
8
+
9
+ import torch
10
+ from torch import nn as nn
11
+ import torch.nn.functional as F
12
+
13
+ from .format import Format, nchw_to
14
+ from .helpers import to_2tuple
15
+ from .patch_embed import resample_patch_embed
16
+
17
+
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
21
+ class HybridEmbed(nn.Module):
22
+ """ CNN Feature Map Embedding
23
+ Extract feature map from CNN, flatten, project to embedding dim.
24
+ """
25
+ output_fmt: Format
26
+ dynamic_img_pad: torch.jit.Final[bool]
27
+
28
+ def __init__(
29
+ self,
30
+ backbone: nn.Module,
31
+ img_size: Union[int, Tuple[int, int]] = 224,
32
+ patch_size: Union[int, Tuple[int, int]] = 1,
33
+ feature_size: Optional[Union[int, Tuple[int, int]]] = None,
34
+ feature_ratio: Optional[Union[int, Tuple[int, int]]] = None,
35
+ in_chans: int = 3,
36
+ embed_dim: int = 768,
37
+ bias: bool = True,
38
+ proj: bool = True,
39
+ flatten: bool = True,
40
+ output_fmt: Optional[str] = None,
41
+ strict_img_size: bool = True,
42
+ dynamic_img_pad: bool = False,
43
+ ):
44
+ super().__init__()
45
+ assert isinstance(backbone, nn.Module)
46
+ self.backbone = backbone
47
+ self.in_chans = in_chans
48
+ (
49
+ self.img_size,
50
+ self.patch_size,
51
+ self.feature_size,
52
+ self.feature_ratio,
53
+ self.feature_dim,
54
+ self.grid_size,
55
+ self.num_patches,
56
+ ) = self._init_backbone(
57
+ img_size=img_size,
58
+ patch_size=patch_size,
59
+ feature_size=feature_size,
60
+ feature_ratio=feature_ratio,
61
+ )
62
+
63
+ if output_fmt is not None:
64
+ self.flatten = False
65
+ self.output_fmt = Format(output_fmt)
66
+ else:
67
+ # flatten spatial dim and transpose to channels last, kept for bwd compat
68
+ self.flatten = flatten
69
+ self.output_fmt = Format.NCHW
70
+ self.strict_img_size = strict_img_size
71
+ self.dynamic_img_pad = dynamic_img_pad
72
+ if not dynamic_img_pad:
73
+ assert self.feature_size[0] % self.patch_size[0] == 0 and self.feature_size[1] % self.patch_size[1] == 0
74
+
75
+ if proj:
76
+ self.proj = nn.Conv2d(
77
+ self.feature_dim,
78
+ embed_dim,
79
+ kernel_size=patch_size,
80
+ stride=patch_size,
81
+ bias=bias,
82
+ )
83
+ else:
84
+ assert self.feature_dim == embed_dim, \
85
+ f'The feature dim ({self.feature_dim} must match embed dim ({embed_dim}) when projection disabled.'
86
+ self.proj = nn.Identity()
87
+
88
+ def _init_backbone(
89
+ self,
90
+ img_size: Union[int, Tuple[int, int]] = 224,
91
+ patch_size: Union[int, Tuple[int, int]] = 1,
92
+ feature_size: Optional[Union[int, Tuple[int, int]]] = None,
93
+ feature_ratio: Optional[Union[int, Tuple[int, int]]] = None,
94
+ feature_dim: Optional[int] = None,
95
+ ):
96
+ img_size = to_2tuple(img_size)
97
+ patch_size = to_2tuple(patch_size)
98
+ if feature_size is None:
99
+ with torch.no_grad():
100
+ # NOTE Most reliable way of determining output dims is to run forward pass
101
+ training = self.backbone.training
102
+ if training:
103
+ self.backbone.eval()
104
+ o = self.backbone(torch.zeros(1, self.in_chans, img_size[0], img_size[1]))
105
+ if isinstance(o, (list, tuple)):
106
+ o = o[-1] # last feature if backbone outputs list/tuple of features
107
+ feature_size = o.shape[-2:]
108
+ feature_dim = o.shape[1]
109
+ self.backbone.train(training)
110
+ feature_ratio = tuple([s // f for s, f in zip(img_size, feature_size)])
111
+ else:
112
+ feature_size = to_2tuple(feature_size)
113
+ feature_ratio = to_2tuple(feature_ratio or 16)
114
+ if feature_dim is None:
115
+ if hasattr(self.backbone, 'feature_info'):
116
+ feature_dim = self.backbone.feature_info.channels()[-1]
117
+ else:
118
+ feature_dim = self.backbone.num_features
119
+ grid_size = tuple([f // p for f, p in zip(feature_size, patch_size)])
120
+ num_patches = grid_size[0] * grid_size[1]
121
+ return img_size, patch_size, feature_size, feature_ratio, feature_dim, grid_size, num_patches
122
+
123
+ def set_input_size(
124
+ self,
125
+ img_size: Optional[Union[int, Tuple[int, int]]] = None,
126
+ patch_size: Optional[Union[int, Tuple[int, int]]] = None,
127
+ feature_size: Optional[Union[int, Tuple[int, int]]] = None,
128
+ feature_ratio: Optional[Union[int, Tuple[int, int]]] = None,
129
+ feature_dim: Optional[int] = None,
130
+ ):
131
+ assert img_size is not None or patch_size is not None
132
+ img_size = img_size or self.img_size
133
+ new_patch_size = None
134
+ if patch_size is not None:
135
+ new_patch_size = to_2tuple(patch_size)
136
+ if new_patch_size is not None and new_patch_size != self.patch_size:
137
+ assert isinstance(self.proj, nn.Conv2d), 'HybridEmbed must have a projection layer to change patch size.'
138
+ with torch.no_grad():
139
+ new_proj = nn.Conv2d(
140
+ self.proj.in_channels,
141
+ self.proj.out_channels,
142
+ kernel_size=new_patch_size,
143
+ stride=new_patch_size,
144
+ bias=self.proj.bias is not None,
145
+ )
146
+ new_proj.weight.copy_(resample_patch_embed(self.proj.weight, new_patch_size, verbose=True))
147
+ if self.proj.bias is not None:
148
+ new_proj.bias.copy_(self.proj.bias)
149
+ self.proj = new_proj
150
+ patch_size = new_patch_size
151
+ patch_size = patch_size or self.patch_size
152
+
153
+ if img_size != self.img_size or patch_size != self.patch_size:
154
+ (
155
+ self.img_size,
156
+ self.patch_size,
157
+ self.feature_size,
158
+ self.feature_ratio,
159
+ self.feature_dim,
160
+ self.grid_size,
161
+ self.num_patches,
162
+ ) = self._init_backbone(
163
+ img_size=img_size,
164
+ patch_size=patch_size,
165
+ feature_size=feature_size,
166
+ feature_ratio=feature_ratio,
167
+ feature_dim=feature_dim,
168
+ )
169
+
170
+ def feat_ratio(self, as_scalar=True) -> Union[Tuple[int, int], int]:
171
+ total_reduction = (
172
+ self.feature_ratio[0] * self.patch_size[0],
173
+ self.feature_ratio[1] * self.patch_size[1]
174
+ )
175
+ if as_scalar:
176
+ return max(total_reduction)
177
+ else:
178
+ return total_reduction
179
+
180
+ def dynamic_feat_size(self, img_size: Tuple[int, int]) -> Tuple[int, int]:
181
+ """ Get feature grid size taking account dynamic padding and backbone network feat reduction
182
+ """
183
+ feat_size = (img_size[0] // self.feature_ratio[0], img_size[1] // self.feature_ratio[1])
184
+ if self.dynamic_img_pad:
185
+ return math.ceil(feat_size[0] / self.patch_size[0]), math.ceil(feat_size[1] / self.patch_size[1])
186
+ else:
187
+ return feat_size[0] // self.patch_size[0], feat_size[1] // self.patch_size[1]
188
+
189
+ @torch.jit.ignore
190
+ def set_grad_checkpointing(self, enable: bool = True):
191
+ if hasattr(self.backbone, 'set_grad_checkpointing'):
192
+ self.backbone.set_grad_checkpointing(enable=enable)
193
+ elif hasattr(self.backbone, 'grad_checkpointing'):
194
+ self.backbone.grad_checkpointing = enable
195
+
196
+ def forward(self, x):
197
+ x = self.backbone(x)
198
+ if isinstance(x, (list, tuple)):
199
+ x = x[-1] # last feature if backbone outputs list/tuple of features
200
+ _, _, H, W = x.shape
201
+ if self.dynamic_img_pad:
202
+ pad_h = (self.patch_size[0] - H % self.patch_size[0]) % self.patch_size[0]
203
+ pad_w = (self.patch_size[1] - W % self.patch_size[1]) % self.patch_size[1]
204
+ x = F.pad(x, (0, pad_w, 0, pad_h))
205
+ x = self.proj(x)
206
+ if self.flatten:
207
+ x = x.flatten(2).transpose(1, 2) # NCHW -> NLC
208
+ elif self.output_fmt != Format.NCHW:
209
+ x = nchw_to(x, self.output_fmt)
210
+ return x
211
+
212
+
213
+ class HybridEmbedWithSize(HybridEmbed):
214
+ """ CNN Feature Map Embedding
215
+ Extract feature map from CNN, flatten, project to embedding dim.
216
+ """
217
+ def __init__(
218
+ self,
219
+ backbone: nn.Module,
220
+ img_size: Union[int, Tuple[int, int]] = 224,
221
+ patch_size: Union[int, Tuple[int, int]] = 1,
222
+ feature_size: Optional[Union[int, Tuple[int, int]]] = None,
223
+ feature_ratio: Optional[Union[int, Tuple[int, int]]] = None,
224
+ in_chans: int = 3,
225
+ embed_dim: int = 768,
226
+ bias=True,
227
+ proj=True,
228
+ ):
229
+ super().__init__(
230
+ backbone=backbone,
231
+ img_size=img_size,
232
+ patch_size=patch_size,
233
+ feature_size=feature_size,
234
+ feature_ratio=feature_ratio,
235
+ in_chans=in_chans,
236
+ embed_dim=embed_dim,
237
+ bias=bias,
238
+ proj=proj,
239
+ )
240
+
241
+ @torch.jit.ignore
242
+ def set_grad_checkpointing(self, enable: bool = True):
243
+ if hasattr(self.backbone, 'set_grad_checkpointing'):
244
+ self.backbone.set_grad_checkpointing(enable=enable)
245
+ elif hasattr(self.backbone, 'grad_checkpointing'):
246
+ self.backbone.grad_checkpointing = enable
247
+
248
+ def forward(self, x) -> Tuple[torch.Tensor, List[int]]:
249
+ x = self.backbone(x)
250
+ if isinstance(x, (list, tuple)):
251
+ x = x[-1] # last feature if backbone outputs list/tuple of features
252
+ x = self.proj(x)
253
+ return x.flatten(2).transpose(1, 2), x.shape[-2:]
reference_medclipseg/biomedclip/layers/inplace_abn.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn as nn
3
+
4
+ try:
5
+ from inplace_abn.functions import inplace_abn, inplace_abn_sync
6
+ has_iabn = True
7
+ except ImportError:
8
+ has_iabn = False
9
+
10
+ def inplace_abn(x, weight, bias, running_mean, running_var,
11
+ training=True, momentum=0.1, eps=1e-05, activation="leaky_relu", activation_param=0.01):
12
+ raise ImportError(
13
+ "Please install InplaceABN:'pip install git+https://github.com/mapillary/inplace_abn.git@v1.0.12'")
14
+
15
+ def inplace_abn_sync(**kwargs):
16
+ inplace_abn(**kwargs)
17
+
18
+
19
+ class InplaceAbn(nn.Module):
20
+ """Activated Batch Normalization
21
+
22
+ This gathers a BatchNorm and an activation function in a single module
23
+
24
+ Parameters
25
+ ----------
26
+ num_features : int
27
+ Number of feature channels in the input and output.
28
+ eps : float
29
+ Small constant to prevent numerical issues.
30
+ momentum : float
31
+ Momentum factor applied to compute running statistics.
32
+ affine : bool
33
+ If `True` apply learned scale and shift transformation after normalization.
34
+ act_layer : str or nn.Module type
35
+ Name or type of the activation functions, one of: `leaky_relu`, `elu`
36
+ act_param : float
37
+ Negative slope for the `leaky_relu` activation.
38
+ """
39
+
40
+ def __init__(self, num_features, eps=1e-5, momentum=0.1, affine=True, apply_act=True,
41
+ act_layer="leaky_relu", act_param=0.01, drop_layer=None):
42
+ super(InplaceAbn, self).__init__()
43
+ self.num_features = num_features
44
+ self.affine = affine
45
+ self.eps = eps
46
+ self.momentum = momentum
47
+ if apply_act:
48
+ if isinstance(act_layer, str):
49
+ assert act_layer in ('leaky_relu', 'elu', 'identity', '')
50
+ self.act_name = act_layer if act_layer else 'identity'
51
+ else:
52
+ # convert act layer passed as type to string
53
+ if act_layer == nn.ELU:
54
+ self.act_name = 'elu'
55
+ elif act_layer == nn.LeakyReLU:
56
+ self.act_name = 'leaky_relu'
57
+ elif act_layer is None or act_layer == nn.Identity:
58
+ self.act_name = 'identity'
59
+ else:
60
+ assert False, f'Invalid act layer {act_layer.__name__} for IABN'
61
+ else:
62
+ self.act_name = 'identity'
63
+ self.act_param = act_param
64
+ if self.affine:
65
+ self.weight = nn.Parameter(torch.ones(num_features))
66
+ self.bias = nn.Parameter(torch.zeros(num_features))
67
+ else:
68
+ self.register_parameter('weight', None)
69
+ self.register_parameter('bias', None)
70
+ self.register_buffer('running_mean', torch.zeros(num_features))
71
+ self.register_buffer('running_var', torch.ones(num_features))
72
+ self.reset_parameters()
73
+
74
+ def reset_parameters(self):
75
+ nn.init.constant_(self.running_mean, 0)
76
+ nn.init.constant_(self.running_var, 1)
77
+ if self.affine:
78
+ nn.init.constant_(self.weight, 1)
79
+ nn.init.constant_(self.bias, 0)
80
+
81
+ def forward(self, x):
82
+ output = inplace_abn(
83
+ x, self.weight, self.bias, self.running_mean, self.running_var,
84
+ self.training, self.momentum, self.eps, self.act_name, self.act_param)
85
+ if isinstance(output, tuple):
86
+ output = output[0]
87
+ return output
reference_medclipseg/biomedclip/layers/interpolate.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Interpolation helpers for timm layers
2
+
3
+ RegularGridInterpolator from https://github.com/sbarratt/torch_interpolations
4
+ Copyright Shane Barratt, Apache 2.0 license
5
+ """
6
+ import torch
7
+ from itertools import product
8
+
9
+
10
+ class RegularGridInterpolator:
11
+ """ Interpolate data defined on a rectilinear grid with even or uneven spacing.
12
+ Produces similar results to scipy RegularGridInterpolator or interp2d
13
+ in 'linear' mode.
14
+
15
+ Taken from https://github.com/sbarratt/torch_interpolations
16
+ """
17
+
18
+ def __init__(self, points, values):
19
+ self.points = points
20
+ self.values = values
21
+
22
+ assert isinstance(self.points, tuple) or isinstance(self.points, list)
23
+ assert isinstance(self.values, torch.Tensor)
24
+
25
+ self.ms = list(self.values.shape)
26
+ self.n = len(self.points)
27
+
28
+ assert len(self.ms) == self.n
29
+
30
+ for i, p in enumerate(self.points):
31
+ assert isinstance(p, torch.Tensor)
32
+ assert p.shape[0] == self.values.shape[i]
33
+
34
+ def __call__(self, points_to_interp):
35
+ assert self.points is not None
36
+ assert self.values is not None
37
+
38
+ assert len(points_to_interp) == len(self.points)
39
+ K = points_to_interp[0].shape[0]
40
+ for x in points_to_interp:
41
+ assert x.shape[0] == K
42
+
43
+ idxs = []
44
+ dists = []
45
+ overalls = []
46
+ for p, x in zip(self.points, points_to_interp):
47
+ idx_right = torch.bucketize(x, p)
48
+ idx_right[idx_right >= p.shape[0]] = p.shape[0] - 1
49
+ idx_left = (idx_right - 1).clamp(0, p.shape[0] - 1)
50
+ dist_left = x - p[idx_left]
51
+ dist_right = p[idx_right] - x
52
+ dist_left[dist_left < 0] = 0.
53
+ dist_right[dist_right < 0] = 0.
54
+ both_zero = (dist_left == 0) & (dist_right == 0)
55
+ dist_left[both_zero] = dist_right[both_zero] = 1.
56
+
57
+ idxs.append((idx_left, idx_right))
58
+ dists.append((dist_left, dist_right))
59
+ overalls.append(dist_left + dist_right)
60
+
61
+ numerator = 0.
62
+ for indexer in product([0, 1], repeat=self.n):
63
+ as_s = [idx[onoff] for onoff, idx in zip(indexer, idxs)]
64
+ bs_s = [dist[1 - onoff] for onoff, dist in zip(indexer, dists)]
65
+ numerator += self.values[as_s] * \
66
+ torch.prod(torch.stack(bs_s), dim=0)
67
+ denominator = torch.prod(torch.stack(overalls), dim=0)
68
+ return numerator / denominator
reference_medclipseg/biomedclip/layers/lambda_layer.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Lambda Layer
2
+
3
+ Paper: `LambdaNetworks: Modeling Long-Range Interactions Without Attention`
4
+ - https://arxiv.org/abs/2102.08602
5
+
6
+ @misc{2102.08602,
7
+ Author = {Irwan Bello},
8
+ Title = {LambdaNetworks: Modeling Long-Range Interactions Without Attention},
9
+ Year = {2021},
10
+ }
11
+
12
+ Status:
13
+ This impl is a WIP. Code snippets in the paper were used as reference but
14
+ good chance some details are missing/wrong.
15
+
16
+ I've only implemented local lambda conv based pos embeddings.
17
+
18
+ For a PyTorch impl that includes other embedding options checkout
19
+ https://github.com/lucidrains/lambda-networks
20
+
21
+ Hacked together by / Copyright 2021 Ross Wightman
22
+ """
23
+ import torch
24
+ from torch import nn
25
+ import torch.nn.functional as F
26
+
27
+ from .grid import ndgrid
28
+ from .helpers import to_2tuple, make_divisible
29
+ from .weight_init import trunc_normal_
30
+
31
+
32
+ def rel_pos_indices(size):
33
+ size = to_2tuple(size)
34
+ pos = torch.stack(ndgrid(torch.arange(size[0]), torch.arange(size[1]))).flatten(1)
35
+ rel_pos = pos[:, None, :] - pos[:, :, None]
36
+ rel_pos[0] += size[0] - 1
37
+ rel_pos[1] += size[1] - 1
38
+ return rel_pos # 2, H * W, H * W
39
+
40
+
41
+ class LambdaLayer(nn.Module):
42
+ """Lambda Layer
43
+
44
+ Paper: `LambdaNetworks: Modeling Long-Range Interactions Without Attention`
45
+ - https://arxiv.org/abs/2102.08602
46
+
47
+ NOTE: intra-depth parameter 'u' is fixed at 1. It did not appear worth the complexity to add.
48
+
49
+ The internal dimensions of the lambda module are controlled via the interaction of several arguments.
50
+ * the output dimension of the module is specified by dim_out, which falls back to input dim if not set
51
+ * the value (v) dimension is set to dim_out // num_heads, the v projection determines the output dim
52
+ * the query (q) and key (k) dimension are determined by
53
+ * dim_head = (dim_out * attn_ratio // num_heads) if dim_head is None
54
+ * q = num_heads * dim_head, k = dim_head
55
+ * as seen above, attn_ratio determines the ratio of q and k relative to the output if dim_head not set
56
+
57
+ Args:
58
+ dim (int): input dimension to the module
59
+ dim_out (int): output dimension of the module, same as dim if not set
60
+ feat_size (Tuple[int, int]): size of input feature_map for relative pos variant H, W
61
+ stride (int): output stride of the module, avg pool used if stride == 2
62
+ num_heads (int): parallel attention heads.
63
+ dim_head (int): dimension of query and key heads, calculated from dim_out * attn_ratio // num_heads if not set
64
+ r (int): local lambda convolution radius. Use lambda conv if set, else relative pos if not. (default: 9)
65
+ qk_ratio (float): ratio of q and k dimensions to output dimension when dim_head not set. (default: 1.0)
66
+ qkv_bias (bool): add bias to q, k, and v projections
67
+ """
68
+ def __init__(
69
+ self, dim, dim_out=None, feat_size=None, stride=1, num_heads=4, dim_head=16, r=9,
70
+ qk_ratio=1.0, qkv_bias=False):
71
+ super().__init__()
72
+ dim_out = dim_out or dim
73
+ assert dim_out % num_heads == 0, ' should be divided by num_heads'
74
+ self.dim_qk = dim_head or make_divisible(dim_out * qk_ratio, divisor=8) // num_heads
75
+ self.num_heads = num_heads
76
+ self.dim_v = dim_out // num_heads
77
+
78
+ self.qkv = nn.Conv2d(
79
+ dim,
80
+ num_heads * self.dim_qk + self.dim_qk + self.dim_v,
81
+ kernel_size=1, bias=qkv_bias)
82
+ self.norm_q = nn.BatchNorm2d(num_heads * self.dim_qk)
83
+ self.norm_v = nn.BatchNorm2d(self.dim_v)
84
+
85
+ if r is not None:
86
+ # local lambda convolution for pos
87
+ self.conv_lambda = nn.Conv3d(1, self.dim_qk, (r, r, 1), padding=(r // 2, r // 2, 0))
88
+ self.pos_emb = None
89
+ self.rel_pos_indices = None
90
+ else:
91
+ # relative pos embedding
92
+ assert feat_size is not None
93
+ feat_size = to_2tuple(feat_size)
94
+ rel_size = [2 * s - 1 for s in feat_size]
95
+ self.conv_lambda = None
96
+ self.pos_emb = nn.Parameter(torch.zeros(rel_size[0], rel_size[1], self.dim_qk))
97
+ self.register_buffer('rel_pos_indices', rel_pos_indices(feat_size), persistent=False)
98
+
99
+ self.pool = nn.AvgPool2d(2, 2) if stride == 2 else nn.Identity()
100
+
101
+ self.reset_parameters()
102
+
103
+ def reset_parameters(self):
104
+ trunc_normal_(self.qkv.weight, std=self.qkv.weight.shape[1] ** -0.5) # fan-in
105
+ if self.conv_lambda is not None:
106
+ trunc_normal_(self.conv_lambda.weight, std=self.dim_qk ** -0.5)
107
+ if self.pos_emb is not None:
108
+ trunc_normal_(self.pos_emb, std=.02)
109
+
110
+ def forward(self, x):
111
+ B, C, H, W = x.shape
112
+ M = H * W
113
+ qkv = self.qkv(x)
114
+ q, k, v = torch.split(qkv, [
115
+ self.num_heads * self.dim_qk, self.dim_qk, self.dim_v], dim=1)
116
+ q = self.norm_q(q).reshape(B, self.num_heads, self.dim_qk, M).transpose(-1, -2) # B, num_heads, M, K
117
+ v = self.norm_v(v).reshape(B, self.dim_v, M).transpose(-1, -2) # B, M, V
118
+ k = F.softmax(k.reshape(B, self.dim_qk, M), dim=-1) # B, K, M
119
+
120
+ content_lam = k @ v # B, K, V
121
+ content_out = q @ content_lam.unsqueeze(1) # B, num_heads, M, V
122
+
123
+ if self.pos_emb is None:
124
+ position_lam = self.conv_lambda(v.reshape(B, 1, H, W, self.dim_v)) # B, H, W, V, K
125
+ position_lam = position_lam.reshape(B, 1, self.dim_qk, H * W, self.dim_v).transpose(2, 3) # B, 1, M, K, V
126
+ else:
127
+ # FIXME relative pos embedding path not fully verified
128
+ pos_emb = self.pos_emb[self.rel_pos_indices[0], self.rel_pos_indices[1]].expand(B, -1, -1, -1)
129
+ position_lam = (pos_emb.transpose(-1, -2) @ v.unsqueeze(1)).unsqueeze(1) # B, 1, M, K, V
130
+ position_out = (q.unsqueeze(-2) @ position_lam).squeeze(-2) # B, num_heads, M, V
131
+
132
+ out = (content_out + position_out).transpose(-1, -2).reshape(B, C, H, W) # B, C (num_heads * V), H, W
133
+ out = self.pool(out)
134
+ return out
reference_medclipseg/biomedclip/layers/layer_scale.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+
4
+
5
+ class LayerScale(nn.Module):
6
+ """ LayerScale on tensors with channels in last-dim.
7
+ """
8
+ def __init__(
9
+ self,
10
+ dim: int,
11
+ init_values: float = 1e-5,
12
+ inplace: bool = False,
13
+ ) -> None:
14
+ super().__init__()
15
+ self.inplace = inplace
16
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
17
+
18
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
19
+ return x.mul_(self.gamma) if self.inplace else x * self.gamma
20
+
21
+
22
+ class LayerScale2d(nn.Module):
23
+ """ LayerScale for tensors with torch 2D NCHW layout.
24
+ """
25
+ def __init__(
26
+ self,
27
+ dim: int,
28
+ init_values: float = 1e-5,
29
+ inplace: bool = False,
30
+ ):
31
+ super().__init__()
32
+ self.inplace = inplace
33
+ self.gamma = nn.Parameter(init_values * torch.ones(dim))
34
+
35
+ def forward(self, x):
36
+ gamma = self.gamma.view(1, -1, 1, 1)
37
+ return x.mul_(gamma) if self.inplace else x * gamma
38
+
reference_medclipseg/biomedclip/layers/linear.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Linear layer (alternate definition)
2
+ """
3
+ import torch
4
+ import torch.nn.functional as F
5
+ from torch import nn as nn
6
+
7
+
8
+ class Linear(nn.Linear):
9
+ r"""Applies a linear transformation to the incoming data: :math:`y = xA^T + b`
10
+
11
+ Wraps torch.nn.Linear to support AMP + torchscript usage by manually casting
12
+ weight & bias to input.dtype to work around an issue w/ torch.addmm in this use case.
13
+ """
14
+ def forward(self, input: torch.Tensor) -> torch.Tensor:
15
+ if torch.jit.is_scripting():
16
+ bias = self.bias.to(dtype=input.dtype) if self.bias is not None else None
17
+ return F.linear(input, self.weight.to(dtype=input.dtype), bias=bias)
18
+ else:
19
+ return F.linear(input, self.weight, self.bias)
reference_medclipseg/biomedclip/layers/median_pool.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Median Pool
2
+ Hacked together by / Copyright 2020 Ross Wightman
3
+ """
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from .helpers import to_2tuple, to_4tuple
7
+
8
+
9
+ class MedianPool2d(nn.Module):
10
+ """ Median pool (usable as median filter when stride=1) module.
11
+
12
+ Args:
13
+ kernel_size: size of pooling kernel, int or 2-tuple
14
+ stride: pool stride, int or 2-tuple
15
+ padding: pool padding, int or 4-tuple (l, r, t, b) as in pytorch F.pad
16
+ same: override padding and enforce same padding, boolean
17
+ """
18
+ def __init__(self, kernel_size=3, stride=1, padding=0, same=False):
19
+ super(MedianPool2d, self).__init__()
20
+ self.k = to_2tuple(kernel_size)
21
+ self.stride = to_2tuple(stride)
22
+ self.padding = to_4tuple(padding) # convert to l, r, t, b
23
+ self.same = same
24
+
25
+ def _padding(self, x):
26
+ if self.same:
27
+ ih, iw = x.size()[2:]
28
+ if ih % self.stride[0] == 0:
29
+ ph = max(self.k[0] - self.stride[0], 0)
30
+ else:
31
+ ph = max(self.k[0] - (ih % self.stride[0]), 0)
32
+ if iw % self.stride[1] == 0:
33
+ pw = max(self.k[1] - self.stride[1], 0)
34
+ else:
35
+ pw = max(self.k[1] - (iw % self.stride[1]), 0)
36
+ pl = pw // 2
37
+ pr = pw - pl
38
+ pt = ph // 2
39
+ pb = ph - pt
40
+ padding = (pl, pr, pt, pb)
41
+ else:
42
+ padding = self.padding
43
+ return padding
44
+
45
+ def forward(self, x):
46
+ x = F.pad(x, self._padding(x), mode='reflect')
47
+ x = x.unfold(2, self.k[0], self.stride[0]).unfold(3, self.k[1], self.stride[1])
48
+ x = x.contiguous().view(x.size()[:4] + (-1,)).median(dim=-1)[0]
49
+ return x
reference_medclipseg/biomedclip/layers/mixed_conv2d.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ PyTorch Mixed Convolution
2
+
3
+ Paper: MixConv: Mixed Depthwise Convolutional Kernels (https://arxiv.org/abs/1907.09595)
4
+
5
+ Hacked together by / Copyright 2020 Ross Wightman
6
+ """
7
+
8
+ import torch
9
+ from torch import nn as nn
10
+
11
+ from .conv2d_same import create_conv2d_pad
12
+
13
+
14
+ def _split_channels(num_chan, num_groups):
15
+ split = [num_chan // num_groups for _ in range(num_groups)]
16
+ split[0] += num_chan - sum(split)
17
+ return split
18
+
19
+
20
+ class MixedConv2d(nn.ModuleDict):
21
+ """ Mixed Grouped Convolution
22
+
23
+ Based on MDConv and GroupedConv in MixNet impl:
24
+ https://github.com/tensorflow/tpu/blob/master/models/official/mnasnet/mixnet/custom_layers.py
25
+ """
26
+ def __init__(self, in_channels, out_channels, kernel_size=3,
27
+ stride=1, padding='', dilation=1, depthwise=False, **kwargs):
28
+ super(MixedConv2d, self).__init__()
29
+
30
+ kernel_size = kernel_size if isinstance(kernel_size, list) else [kernel_size]
31
+ num_groups = len(kernel_size)
32
+ in_splits = _split_channels(in_channels, num_groups)
33
+ out_splits = _split_channels(out_channels, num_groups)
34
+ self.in_channels = sum(in_splits)
35
+ self.out_channels = sum(out_splits)
36
+ for idx, (k, in_ch, out_ch) in enumerate(zip(kernel_size, in_splits, out_splits)):
37
+ conv_groups = in_ch if depthwise else 1
38
+ # use add_module to keep key space clean
39
+ self.add_module(
40
+ str(idx),
41
+ create_conv2d_pad(
42
+ in_ch, out_ch, k, stride=stride,
43
+ padding=padding, dilation=dilation, groups=conv_groups, **kwargs)
44
+ )
45
+ self.splits = in_splits
46
+
47
+ def forward(self, x):
48
+ x_split = torch.split(x, self.splits, 1)
49
+ x_out = [c(x_split[i]) for i, c in enumerate(self.values())]
50
+ x = torch.cat(x_out, 1)
51
+ return x
reference_medclipseg/biomedclip/layers/ml_decoder.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ from torch import nn
5
+ from torch import nn, Tensor
6
+ from torch.nn.modules.transformer import _get_activation_fn
7
+
8
+
9
+ def add_ml_decoder_head(model):
10
+ if hasattr(model, 'global_pool') and hasattr(model, 'fc'): # most CNN models, like Resnet50
11
+ model.global_pool = nn.Identity()
12
+ del model.fc
13
+ num_classes = model.num_classes
14
+ num_features = model.num_features
15
+ model.fc = MLDecoder(num_classes=num_classes, initial_num_features=num_features)
16
+ elif hasattr(model, 'global_pool') and hasattr(model, 'classifier'): # EfficientNet
17
+ model.global_pool = nn.Identity()
18
+ del model.classifier
19
+ num_classes = model.num_classes
20
+ num_features = model.num_features
21
+ model.classifier = MLDecoder(num_classes=num_classes, initial_num_features=num_features)
22
+ elif 'RegNet' in model._get_name() or 'TResNet' in model._get_name(): # hasattr(model, 'head')
23
+ del model.head
24
+ num_classes = model.num_classes
25
+ num_features = model.num_features
26
+ model.head = MLDecoder(num_classes=num_classes, initial_num_features=num_features)
27
+ else:
28
+ print("Model code-writing is not aligned currently with ml-decoder")
29
+ exit(-1)
30
+ if hasattr(model, 'drop_rate'): # Ml-Decoder has inner dropout
31
+ model.drop_rate = 0
32
+ return model
33
+
34
+
35
+ class TransformerDecoderLayerOptimal(nn.Module):
36
+ def __init__(self, d_model, nhead=8, dim_feedforward=2048, dropout=0.1, activation="relu",
37
+ layer_norm_eps=1e-5) -> None:
38
+ super(TransformerDecoderLayerOptimal, self).__init__()
39
+ self.norm1 = nn.LayerNorm(d_model, eps=layer_norm_eps)
40
+ self.dropout = nn.Dropout(dropout)
41
+ self.dropout1 = nn.Dropout(dropout)
42
+ self.dropout2 = nn.Dropout(dropout)
43
+ self.dropout3 = nn.Dropout(dropout)
44
+
45
+ self.multihead_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
46
+
47
+ # Implementation of Feedforward model
48
+ self.linear1 = nn.Linear(d_model, dim_feedforward)
49
+ self.linear2 = nn.Linear(dim_feedforward, d_model)
50
+
51
+ self.norm2 = nn.LayerNorm(d_model, eps=layer_norm_eps)
52
+ self.norm3 = nn.LayerNorm(d_model, eps=layer_norm_eps)
53
+
54
+ self.activation = _get_activation_fn(activation)
55
+
56
+ def __setstate__(self, state):
57
+ if 'activation' not in state:
58
+ state['activation'] = torch.nn.functional.relu
59
+ super(TransformerDecoderLayerOptimal, self).__setstate__(state)
60
+
61
+ def forward(self, tgt: Tensor, memory: Tensor, tgt_mask: Optional[Tensor] = None,
62
+ memory_mask: Optional[Tensor] = None,
63
+ tgt_key_padding_mask: Optional[Tensor] = None,
64
+ memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:
65
+ tgt = tgt + self.dropout1(tgt)
66
+ tgt = self.norm1(tgt)
67
+ tgt2 = self.multihead_attn(tgt, memory, memory)[0]
68
+ tgt = tgt + self.dropout2(tgt2)
69
+ tgt = self.norm2(tgt)
70
+ tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
71
+ tgt = tgt + self.dropout3(tgt2)
72
+ tgt = self.norm3(tgt)
73
+ return tgt
74
+
75
+
76
+ # class ExtrapClasses(object):
77
+ # def __init__(self, num_queries: int, group_size: int):
78
+ # self.num_queries = num_queries
79
+ # self.group_size = group_size
80
+ #
81
+ # def __call__(self, h: torch.Tensor, class_embed_w: torch.Tensor, class_embed_b: torch.Tensor, out_extrap:
82
+ # torch.Tensor):
83
+ # # h = h.unsqueeze(-1).expand(-1, -1, -1, self.group_size)
84
+ # h = h[..., None].repeat(1, 1, 1, self.group_size) # torch.Size([bs, 5, 768, groups])
85
+ # w = class_embed_w.view((self.num_queries, h.shape[2], self.group_size))
86
+ # out = (h * w).sum(dim=2) + class_embed_b
87
+ # out = out.view((h.shape[0], self.group_size * self.num_queries))
88
+ # return out
89
+
90
+ class MLDecoder(nn.Module):
91
+ def __init__(self, num_classes, num_of_groups=-1, decoder_embedding=768, initial_num_features=2048):
92
+ super(MLDecoder, self).__init__()
93
+ embed_len_decoder = 100 if num_of_groups < 0 else num_of_groups
94
+ if embed_len_decoder > num_classes:
95
+ embed_len_decoder = num_classes
96
+ self.embed_len_decoder = embed_len_decoder
97
+
98
+ # switching to 768 initial embeddings
99
+ decoder_embedding = 768 if decoder_embedding < 0 else decoder_embedding
100
+ self.embed_standart = nn.Linear(initial_num_features, decoder_embedding)
101
+
102
+ # decoder
103
+ decoder_dropout = 0.1
104
+ num_layers_decoder = 1
105
+ dim_feedforward = 2048
106
+ layer_decode = TransformerDecoderLayerOptimal(d_model=decoder_embedding,
107
+ dim_feedforward=dim_feedforward, dropout=decoder_dropout)
108
+ self.decoder = nn.TransformerDecoder(layer_decode, num_layers=num_layers_decoder)
109
+
110
+ # non-learnable queries
111
+ self.query_embed = nn.Embedding(embed_len_decoder, decoder_embedding)
112
+ self.query_embed.requires_grad_(False)
113
+
114
+ # group fully-connected
115
+ self.num_classes = num_classes
116
+ self.duplicate_factor = int(num_classes / embed_len_decoder + 0.999)
117
+ self.duplicate_pooling = torch.nn.Parameter(
118
+ torch.Tensor(embed_len_decoder, decoder_embedding, self.duplicate_factor))
119
+ self.duplicate_pooling_bias = torch.nn.Parameter(torch.Tensor(num_classes))
120
+ torch.nn.init.xavier_normal_(self.duplicate_pooling)
121
+ torch.nn.init.constant_(self.duplicate_pooling_bias, 0)
122
+
123
+ def forward(self, x):
124
+ if len(x.shape) == 4: # [bs,2048, 7,7]
125
+ embedding_spatial = x.flatten(2).transpose(1, 2)
126
+ else: # [bs, 197,468]
127
+ embedding_spatial = x
128
+ embedding_spatial_786 = self.embed_standart(embedding_spatial)
129
+ embedding_spatial_786 = torch.nn.functional.relu(embedding_spatial_786, inplace=True)
130
+
131
+ bs = embedding_spatial_786.shape[0]
132
+ query_embed = self.query_embed.weight
133
+ # tgt = query_embed.unsqueeze(1).repeat(1, bs, 1)
134
+ tgt = query_embed.unsqueeze(1).expand(-1, bs, -1) # no allocation of memory with expand
135
+ h = self.decoder(tgt, embedding_spatial_786.transpose(0, 1)) # [embed_len_decoder, batch, 768]
136
+ h = h.transpose(0, 1)
137
+
138
+ out_extrap = torch.zeros(h.shape[0], h.shape[1], self.duplicate_factor, device=h.device, dtype=h.dtype)
139
+ for i in range(self.embed_len_decoder): # group FC
140
+ h_i = h[:, i, :]
141
+ w_i = self.duplicate_pooling[i, :, :]
142
+ out_extrap[:, i, :] = torch.matmul(h_i, w_i)
143
+ h_out = out_extrap.flatten(1)[:, :self.num_classes]
144
+ h_out += self.duplicate_pooling_bias
145
+ logits = h_out
146
+ return logits