Spaces:
Running
Running
Upload 314 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +5 -0
- README.md +71 -56
- app.py +122 -351
- medclipseg.py +172 -0
- prepare_medclip_data.py +115 -0
- processing.py +841 -92
- reference_medclipseg/LICENSE +201 -0
- reference_medclipseg/README.md +141 -0
- reference_medclipseg/biomedclip/biomedclip.py +80 -0
- reference_medclipseg/biomedclip/layers/__init__.py +62 -0
- reference_medclipseg/biomedclip/layers/activations.py +173 -0
- reference_medclipseg/biomedclip/layers/activations_me.py +208 -0
- reference_medclipseg/biomedclip/layers/adaptive_avgmax_pool.py +183 -0
- reference_medclipseg/biomedclip/layers/attention2d.py +351 -0
- reference_medclipseg/biomedclip/layers/attention_pool.py +105 -0
- reference_medclipseg/biomedclip/layers/attention_pool2d.py +278 -0
- reference_medclipseg/biomedclip/layers/blur_pool.py +91 -0
- reference_medclipseg/biomedclip/layers/bottleneck_attn.py +157 -0
- reference_medclipseg/biomedclip/layers/cbam.py +112 -0
- reference_medclipseg/biomedclip/layers/classifier.py +283 -0
- reference_medclipseg/biomedclip/layers/cond_conv2d.py +123 -0
- reference_medclipseg/biomedclip/layers/config.py +165 -0
- reference_medclipseg/biomedclip/layers/conv2d_same.py +110 -0
- reference_medclipseg/biomedclip/layers/conv_bn_act.py +92 -0
- reference_medclipseg/biomedclip/layers/create_act.py +138 -0
- reference_medclipseg/biomedclip/layers/create_attn.py +89 -0
- reference_medclipseg/biomedclip/layers/create_conv2d.py +36 -0
- reference_medclipseg/biomedclip/layers/create_norm.py +60 -0
- reference_medclipseg/biomedclip/layers/create_norm_act.py +95 -0
- reference_medclipseg/biomedclip/layers/drop.py +182 -0
- reference_medclipseg/biomedclip/layers/eca.py +145 -0
- reference_medclipseg/biomedclip/layers/evo_norm.py +352 -0
- reference_medclipseg/biomedclip/layers/fast_norm.py +206 -0
- reference_medclipseg/biomedclip/layers/filter_response_norm.py +68 -0
- reference_medclipseg/biomedclip/layers/format.py +58 -0
- reference_medclipseg/biomedclip/layers/gather_excite.py +90 -0
- reference_medclipseg/biomedclip/layers/global_context.py +67 -0
- reference_medclipseg/biomedclip/layers/grid.py +49 -0
- reference_medclipseg/biomedclip/layers/grn.py +39 -0
- reference_medclipseg/biomedclip/layers/halo_attn.py +233 -0
- reference_medclipseg/biomedclip/layers/helpers.py +43 -0
- reference_medclipseg/biomedclip/layers/hybrid_embed.py +253 -0
- reference_medclipseg/biomedclip/layers/inplace_abn.py +87 -0
- reference_medclipseg/biomedclip/layers/interpolate.py +68 -0
- reference_medclipseg/biomedclip/layers/lambda_layer.py +134 -0
- reference_medclipseg/biomedclip/layers/layer_scale.py +38 -0
- reference_medclipseg/biomedclip/layers/linear.py +19 -0
- reference_medclipseg/biomedclip/layers/median_pool.py +49 -0
- reference_medclipseg/biomedclip/layers/mixed_conv2d.py +51 -0
- 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:
|
| 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 |
-
#
|
| 14 |
|
| 15 |
-
|
| 16 |
-
organ of Corti and quantifies them **per frequency region**, separating
|
| 17 |
-
**IHC-innervating** from **OHC-innervating** fibers.
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
- **
|
| 28 |
-
|
| 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 |
-
|
| 38 |
-
|
| 39 |
-
|
| 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 |
-
|
| 54 |
-
|
| 55 |
-
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
##
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
quantification (number of fibers, diameter, length, branch points, area
|
| 9 |
-
covered) plus a black-background skeleton image and an Excel workbook.
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 92 |
if file_obj is None:
|
| 93 |
-
return
|
| 94 |
-
gr.update(), None, None, "Please upload a CZI or TIFF file.")
|
| 95 |
try:
|
| 96 |
-
img = P.load_image(file_obj
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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 |
-
|
| 259 |
-
gr.
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 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 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 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 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 211 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 216 |
-
for i,
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 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 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
dz, dy, dx = voxel
|
| 278 |
mask = _threshold_volume(nf_vol, sensitivity)
|
| 279 |
mask = remove_small_objects(mask, min_object_vox)
|
| 280 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 316 |
-
|
| 317 |
-
"""Return (ihc_roi, ohc_roi) boolean 2D masks split by a straight line.
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
"""
|
| 322 |
ny, nx = shape_yx
|
| 323 |
-
low = np.zeros((ny, nx), bool)
|
| 324 |
if axis.upper() == "Y":
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
low[:b] = True
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
if dilate:
|
| 412 |
-
|
| 413 |
-
|
| 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 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+

|
| 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
|