Add new regression and ensemble experiment configurations for V2-M and OrthoBridge
- Introduced multiple regression experiment configurations targeting vf_md, including: - cd_solo_reg_set.json: CD tower only regression setup. - img_solo_reg_set.json: Image tower only regression setup. - reg_head_epoch_sweep.json: Baseline regression sweeps at different epochs (50, 75, 100). - reg_head_set.json: Various regression setups including baseline and OrthoBridge configurations. - single_eye_reg.json: Single-eye regression setup for worst-eye aggregation analysis. - Added ensemble configurations for OrthoBridge with different inner bridges: - ortho_alts_ensemble.json: Ensemble tests with ConcatBridge, PairwiseAdditiveBridge, and GatedAdditiveBridge. - ortho_alts_tritower.json: Tritower tests with the same inner bridges. - Created V2-M specific configurations: - baseline_reg_nt50.json: Regression baseline with V2-M backbone. - geom_vec_gt.json and geom_vec_unet.json: Geometry vector injection experiments with V2-M. - single_l1_bridges.json: Single-eye ensemble experiments with various bridge types. - tritower_geom_gt.json: Tritower setup with GT contour-rasterized masks. - Promoted existing experiments to higher repetitions for robustness.
This commit is contained in:
@@ -12,10 +12,11 @@ from torchvision import models, transforms
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
REFUGELIKE_BACKBONE_PATH = _REPO_ROOT / "models/v2/refuge/refugelike_backbone.pt"
|
||||
REFUGE_DENSENET_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_densenet_backbone.pt"
|
||||
REFUGE_EFFICIENT_B0_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficient_b0_backbone.pt"
|
||||
REFUGE_EFFICIENT_B7_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficient_b7_backbone.pt"
|
||||
REFUGELIKE_BACKBONE_PATH = _REPO_ROOT / "models/v2/refuge/refugelike_backbone.pt"
|
||||
REFUGE_DENSENET_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_densenet_backbone.pt"
|
||||
REFUGE_EFFICIENTNET_B0_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficientnet_b0_backbone.pt"
|
||||
REFUGE_EFFICIENTNET_B7_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficientnet_b7_backbone.pt"
|
||||
REFUGE_EFFICIENTNET_V2_M_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficientnet_v2_m_backbone.pt"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -109,6 +110,30 @@ BACKBONES: Dict[str, BackboneSpec] = {
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"efficientnet_b7": BackboneSpec(
|
||||
ctor=models.efficientnet_b7,
|
||||
weights_default=models.EfficientNet_B7_Weights.DEFAULT,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"efficientnet_v2_s": BackboneSpec(
|
||||
ctor=models.efficientnet_v2_s,
|
||||
weights_default=models.EfficientNet_V2_S_Weights.DEFAULT,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"efficientnet_v2_m": BackboneSpec(
|
||||
ctor=models.efficientnet_v2_m,
|
||||
weights_default=models.EfficientNet_V2_M_Weights.DEFAULT,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"efficientnet_v2_l": BackboneSpec(
|
||||
ctor=models.efficientnet_v2_l,
|
||||
weights_default=models.EfficientNet_V2_L_Weights.DEFAULT,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"resnet18": BackboneSpec(
|
||||
ctor=models.resnet18,
|
||||
weights_default=models.ResNet18_Weights.DEFAULT,
|
||||
@@ -157,31 +182,94 @@ BACKBONES: Dict[str, BackboneSpec] = {
|
||||
strip=_strip_densenet,
|
||||
blocks=_blocks_densenet,
|
||||
),
|
||||
"refuge_efficient_b0": BackboneSpec(
|
||||
"refuge_efficientnet_b0": BackboneSpec(
|
||||
ctor=models.efficientnet_b0,
|
||||
weights_default=None,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"refuge_efficient_b7": BackboneSpec(
|
||||
"refuge_efficientnet_b7": BackboneSpec(
|
||||
ctor=models.efficientnet_b7,
|
||||
weights_default=None,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
"refuge_efficientnet_v2_m": BackboneSpec(
|
||||
ctor=models.efficientnet_v2_m,
|
||||
weights_default=None,
|
||||
strip=_strip_efficientnet,
|
||||
blocks=_blocks_efficientnet,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
_TIMM_CONVNEXTV2_VARIANTS = (
|
||||
"convnextv2_atto",
|
||||
"convnextv2_femto",
|
||||
"convnextv2_pico",
|
||||
"convnextv2_nano",
|
||||
"convnextv2_tiny",
|
||||
"convnextv2_base",
|
||||
"convnextv2_large",
|
||||
"convnextv2_huge",
|
||||
)
|
||||
|
||||
|
||||
def _is_timm_backbone(name: str) -> bool:
|
||||
return name in _TIMM_CONVNEXTV2_VARIANTS
|
||||
|
||||
|
||||
def _build_timm_backbone(name: str, freeze_ratio: float) -> tuple[nn.Module, int, list]:
|
||||
"""Build a timm-sourced backbone (ConvNeXt-V2 family).
|
||||
|
||||
Uses num_classes=0 + global_pool="avg" so the model returns a pooled
|
||||
(B, num_features) tensor directly — no head to strip. Freezable blocks
|
||||
are [stem, stage_0, stage_1, stage_2, stage_3].
|
||||
"""
|
||||
try:
|
||||
import timm
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
f"backbone {name!r} requires the `timm` package "
|
||||
"(pip install timm)"
|
||||
) from e
|
||||
|
||||
m = timm.create_model(name, pretrained=True, num_classes=0, global_pool="avg")
|
||||
out_dim = int(getattr(m, "num_features", 0))
|
||||
if not out_dim:
|
||||
raise RuntimeError(f"timm model {name!r} did not expose num_features")
|
||||
|
||||
blocks: list[nn.Module] = []
|
||||
if hasattr(m, "stem"):
|
||||
blocks.append(m.stem)
|
||||
if hasattr(m, "stages"):
|
||||
blocks.extend(list(m.stages))
|
||||
if not blocks:
|
||||
raise RuntimeError(
|
||||
f"timm model {name!r} did not expose .stem / .stages — "
|
||||
"extend _build_timm_backbone to support this architecture."
|
||||
)
|
||||
|
||||
fr = max(0.0, min(1.0, float(freeze_ratio)))
|
||||
n_freeze = int(math.floor(len(blocks) * fr))
|
||||
for b in blocks[:n_freeze]:
|
||||
for p in b.parameters():
|
||||
p.requires_grad = False
|
||||
|
||||
return m, out_dim, blocks
|
||||
|
||||
|
||||
def list_names() -> List[str]:
|
||||
return list(BACKBONES.keys())
|
||||
return list(BACKBONES.keys()) + list(_TIMM_CONVNEXTV2_VARIANTS)
|
||||
|
||||
|
||||
def load_backbone_weights(key: str, model: nn.Module) -> None:
|
||||
paths = {
|
||||
"refugelike": REFUGELIKE_BACKBONE_PATH,
|
||||
"refuge_densenet": REFUGE_DENSENET_PATH,
|
||||
"refuge_efficient_b0": REFUGE_EFFICIENT_B0_PATH,
|
||||
"refuge_efficient_b7": REFUGE_EFFICIENT_B7_PATH,
|
||||
"refugelike": REFUGELIKE_BACKBONE_PATH,
|
||||
"refuge_densenet": REFUGE_DENSENET_PATH,
|
||||
"refuge_efficientnet_b0": REFUGE_EFFICIENTNET_B0_PATH,
|
||||
"refuge_efficientnet_b7": REFUGE_EFFICIENTNET_B7_PATH,
|
||||
"refuge_efficientnet_v2_m": REFUGE_EFFICIENTNET_V2_M_PATH,
|
||||
}
|
||||
path = paths.get(key)
|
||||
if path is None:
|
||||
@@ -202,6 +290,8 @@ def build_backbone(name: str, freeze_ratio: float = 0.0) -> tuple[nn.Module, int
|
||||
freezable units — callers use it to dynamically adjust freeze_ratio later.
|
||||
"""
|
||||
key = (name or "").lower()
|
||||
if _is_timm_backbone(key):
|
||||
return _build_timm_backbone(key, freeze_ratio)
|
||||
if key not in BACKBONES:
|
||||
raise ValueError(f"Unknown backbone '{name}'. Available: {list_names()}")
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
"""Generic explainability primitives — GradCAM + region-of-interest analysis
|
||||
+ column-shuffling permutation importance.
|
||||
|
||||
This module is dataset-agnostic. All PAPILA-specific knowledge (where the disc
|
||||
contour lives, eye orientation conventions, clinical feature names, etc.) is
|
||||
supplied by the profile through optional methods consumed via getattr — see
|
||||
v4.classes.profiles.v4papila.ImageDataView and ClinicalDataView for examples.
|
||||
|
||||
Public surface
|
||||
--------------
|
||||
GradCAM — hooks-based class-activation maps via fwd/bwd hooks
|
||||
overlay_gradcam — render a heatmap blended onto a PIL image
|
||||
cam_region_stats — quantify CAM attention inside/outside a binary ROI
|
||||
cam_region_patch — crop a square patch centered on the ROI centroid
|
||||
permutation_importance — per-feature drop-in-score under column shuffling
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from matplotlib import cm
|
||||
from PIL import Image
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GradCAM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class GradCAM:
|
||||
"""Minimal GradCAM via forward/backward hooks.
|
||||
|
||||
Architecture-agnostic: the caller supplies a forward_fn that returns a
|
||||
(1, num_classes) logits tensor, so this class doesn't need to know whether
|
||||
the model takes (img,), (img, meta), or composed tower/bridge/head inputs.
|
||||
|
||||
Usage
|
||||
-----
|
||||
cam_module = GradCAM(target_layer)
|
||||
cam, pred = cam_module.compute(
|
||||
forward_fn=lambda: head(bridge([tower_img(x), tower_cd(c)])),
|
||||
output_shape=(224, 224),
|
||||
)
|
||||
cam_module.remove()
|
||||
"""
|
||||
|
||||
def __init__(self, target_layer: nn.Module) -> None:
|
||||
self._acts: torch.Tensor | None = None
|
||||
self._grads: torch.Tensor | None = None
|
||||
self._h1 = target_layer.register_forward_hook(self._save_acts)
|
||||
self._h2 = target_layer.register_full_backward_hook(self._save_grads)
|
||||
|
||||
def _save_acts(self, _module, _inputs, output):
|
||||
self._acts = output.detach()
|
||||
|
||||
def _save_grads(self, _module, _grad_in, grad_out):
|
||||
self._grads = grad_out[0].detach()
|
||||
|
||||
def compute(
|
||||
self,
|
||||
forward_fn: Callable[[], torch.Tensor],
|
||||
*,
|
||||
output_shape: tuple[int, int],
|
||||
target_class: int | None = None,
|
||||
) -> tuple[np.ndarray, int]:
|
||||
"""Run forward via forward_fn, backward against target_class, return CAM.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
forward_fn
|
||||
Zero-arg callable returning a (1, C) logits tensor. Caller is
|
||||
responsible for placing model in eval mode and zeroing grads.
|
||||
output_shape
|
||||
(H, W) at which the CAM is bilinearly upsampled. Typically the
|
||||
eval-crop image size, e.g. (224, 224).
|
||||
target_class
|
||||
Class index to backprop against. If None, uses the predicted class.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(cam, pred)
|
||||
cam : float32 ndarray of shape output_shape, normalised to [0, 1]
|
||||
pred : int, the predicted class index
|
||||
"""
|
||||
with torch.enable_grad():
|
||||
out = forward_fn()
|
||||
pred = int(out.argmax(1).item())
|
||||
tc = pred if target_class is None else target_class
|
||||
out[0, tc].backward()
|
||||
|
||||
assert self._grads is not None and self._acts is not None, (
|
||||
"GradCAM hooks did not fire — check that target_layer is in the "
|
||||
"forward path of forward_fn()."
|
||||
)
|
||||
weights = self._grads.mean(dim=(2, 3), keepdim=True)
|
||||
cam = F.relu((weights * self._acts).sum(dim=1, keepdim=True))
|
||||
cam = F.interpolate(cam, output_shape, mode="bilinear", align_corners=False)
|
||||
cam_np = cam.squeeze().cpu().numpy()
|
||||
lo, hi = cam_np.min(), cam_np.max()
|
||||
return ((cam_np - lo) / (hi - lo + 1e-8)).astype(np.float32), pred
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Detach the forward/backward hooks. Call when done with this instance."""
|
||||
self._h1.remove()
|
||||
self._h2.remove()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Visualisation
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def overlay_gradcam(pil: Image.Image, cam: np.ndarray, alpha: float = 0.45) -> Image.Image:
|
||||
"""Blend a jet-colored heatmap of `cam` onto `pil` at alpha blending weight."""
|
||||
cam_u8 = (np.clip(cam, 0.0, 1.0) * 255).astype(np.uint8)
|
||||
cam_r = np.array(Image.fromarray(cam_u8).resize(pil.size, Image.BILINEAR)) / 255.0
|
||||
colored = (cm.jet(cam_r)[:, :, :3] * 255).astype(np.uint8)
|
||||
return Image.blend(pil.convert("RGB"), Image.fromarray(colored), alpha)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Region-of-interest analysis (generic — no PAPILA knowledge)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def cam_region_stats(cam: np.ndarray, roi_mask: np.ndarray) -> dict[str, float]:
|
||||
"""Quantify CAM attention restricted to / outside a binary ROI mask.
|
||||
|
||||
The CAM and the mask must be at the same resolution.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with:
|
||||
inside_mean : mean CAM intensity inside the ROI
|
||||
outside_mean : mean CAM intensity outside the ROI
|
||||
inside_fraction : sum(CAM inside) / sum(CAM total); fraction of total
|
||||
attention that falls inside the ROI
|
||||
roi_area_frac : ROI area / total area; how much of the image the ROI
|
||||
covers — useful for normalising inside_fraction
|
||||
"""
|
||||
if cam.shape != roi_mask.shape:
|
||||
raise ValueError(
|
||||
f"cam shape {cam.shape} != roi_mask shape {roi_mask.shape}"
|
||||
)
|
||||
mask = roi_mask.astype(bool)
|
||||
cam_sum = float(cam.sum())
|
||||
inside_sum = float(cam[mask].sum()) if mask.any() else 0.0
|
||||
outside_sum = cam_sum - inside_sum
|
||||
n_inside = int(mask.sum())
|
||||
n_outside = int(mask.size - n_inside)
|
||||
return {
|
||||
"inside_mean": inside_sum / max(1, n_inside),
|
||||
"outside_mean": outside_sum / max(1, n_outside),
|
||||
"inside_fraction": inside_sum / cam_sum if cam_sum > 0 else 0.0,
|
||||
"roi_area_frac": n_inside / mask.size,
|
||||
}
|
||||
|
||||
|
||||
def cam_region_patch(
|
||||
cam: np.ndarray,
|
||||
roi_mask: np.ndarray,
|
||||
*,
|
||||
span: float = 5.0,
|
||||
patch_size: int = 96,
|
||||
) -> tuple[np.ndarray | None, float | None]:
|
||||
"""Crop a square patch centered on the ROI centroid.
|
||||
|
||||
The crop side is `span × roi_radius` (in CAM pixels), where roi_radius is
|
||||
derived from the mask area assuming a circular ROI. The output is resized
|
||||
to (patch_size, patch_size).
|
||||
|
||||
Returns
|
||||
-------
|
||||
(patch, roi_radius_in_patch_pixels)
|
||||
patch : float32 ndarray of shape (patch_size, patch_size), values in
|
||||
[0, 1]. Padded with zeros if the centered crop window extends
|
||||
outside the CAM.
|
||||
roi_radius_in_patch_pixels : float, the ROI radius re-expressed in
|
||||
output-patch pixels (useful for drawing ROI circles in figures).
|
||||
Returns (None, None) if the mask is empty.
|
||||
"""
|
||||
mask = roi_mask.astype(bool)
|
||||
if not mask.any():
|
||||
return None, None
|
||||
|
||||
ys, xs = np.where(mask)
|
||||
cy, cx = ys.mean(), xs.mean()
|
||||
roi_r = float(np.sqrt(mask.sum() / np.pi))
|
||||
half = max(1, int(round(span * roi_r / 2)))
|
||||
h, w = cam.shape
|
||||
|
||||
y0, y1 = int(round(cy)) - half, int(round(cy)) + half
|
||||
x0, x1 = int(round(cx)) - half, int(round(cx)) + half
|
||||
pt = max(0, -y0)
|
||||
pb = max(0, y1 - h)
|
||||
pl = max(0, -x0)
|
||||
pr = max(0, x1 - w)
|
||||
cam_pad = np.pad(cam, ((pt, pb), (pl, pr)), constant_values=0.0)
|
||||
window = cam_pad[y0 + pt: y1 + pt, x0 + pl: x1 + pl]
|
||||
|
||||
patch = np.array(
|
||||
Image.fromarray((np.clip(window, 0, 1) * 255).astype(np.uint8))
|
||||
.resize((patch_size, patch_size), Image.BILINEAR)
|
||||
) / 255.0
|
||||
roi_r_out = patch_size * roi_r / (2 * half)
|
||||
return patch.astype(np.float32), roi_r_out
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Permutation importance (column-shuffling)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def permutation_importance(
|
||||
*,
|
||||
score_fn: Callable[[np.ndarray], float],
|
||||
X: np.ndarray,
|
||||
groups: Sequence[Sequence[int]] | None = None,
|
||||
n_permutations: int = 30,
|
||||
seed: int = 0,
|
||||
feature_names: Sequence[str] | None = None,
|
||||
) -> dict:
|
||||
"""Per-feature drop-in-score from column shuffling.
|
||||
|
||||
Generic and feature-agnostic — the caller wires the model (or any
|
||||
arbitrary scoring pipeline) into ``score_fn``. Higher score = better;
|
||||
drop = baseline_score − score(X_with_column_permuted).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
score_fn
|
||||
Callable taking the input matrix ``X`` (shape (N, F)) and returning
|
||||
a scalar metric. Caller is responsible for any constants the metric
|
||||
depends on (e.g. labels, held-out image embeddings).
|
||||
X
|
||||
Float ndarray of shape (N, F). The base matrix to permute.
|
||||
groups
|
||||
Optional list of column-index groups. If provided, each group is
|
||||
permuted together (useful when several model dims encode one
|
||||
conceptual feature, e.g. one-hot encodings of a categorical column).
|
||||
Defaults to one group per column.
|
||||
n_permutations
|
||||
Number of independent shuffles per group; mean and std of the drop
|
||||
are reported across these.
|
||||
seed
|
||||
Seed for the shuffling RNG.
|
||||
feature_names
|
||||
Optional human-readable names, one per group. If not provided, names
|
||||
default to f"group_{i}" or f"col_{j}" when groups is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict with keys:
|
||||
baseline : float, the score on unmodified X
|
||||
mean_drop : ndarray of shape (G,), mean (baseline − shuffled) per group
|
||||
std_drop : ndarray of shape (G,), std of per-permutation drops
|
||||
feature_names : list[str] of length G
|
||||
groups : list[list[int]] of resolved column groups
|
||||
"""
|
||||
if X.ndim != 2:
|
||||
raise ValueError(f"X must be 2-D (N, F); got shape {X.shape}")
|
||||
n, f_dim = X.shape
|
||||
|
||||
if groups is None:
|
||||
resolved_groups: list[list[int]] = [[j] for j in range(f_dim)]
|
||||
else:
|
||||
resolved_groups = [list(g) for g in groups]
|
||||
all_cols = [c for g in resolved_groups for c in g]
|
||||
if any(c < 0 or c >= f_dim for c in all_cols):
|
||||
raise ValueError(
|
||||
f"groups contain column indices outside [0, {f_dim})"
|
||||
)
|
||||
|
||||
n_groups = len(resolved_groups)
|
||||
if feature_names is None:
|
||||
names = (
|
||||
[f"col_{g[0]}" for g in resolved_groups] if groups is None
|
||||
else [f"group_{i}" for i in range(n_groups)]
|
||||
)
|
||||
else:
|
||||
names = list(feature_names)
|
||||
if len(names) != n_groups:
|
||||
raise ValueError(
|
||||
f"feature_names has length {len(names)} but there are "
|
||||
f"{n_groups} groups"
|
||||
)
|
||||
|
||||
rng = np.random.default_rng(seed)
|
||||
baseline = float(score_fn(X))
|
||||
drops = np.empty((n_groups, n_permutations), dtype=float)
|
||||
for gi, cols in enumerate(resolved_groups):
|
||||
for p in range(n_permutations):
|
||||
X_perm = X.copy()
|
||||
perm = rng.permutation(n)
|
||||
X_perm[:, cols] = X_perm[perm][:, cols]
|
||||
drops[gi, p] = baseline - float(score_fn(X_perm))
|
||||
|
||||
return {
|
||||
"baseline": baseline,
|
||||
"mean_drop": drops.mean(axis=1),
|
||||
"std_drop": drops.std(axis=1),
|
||||
"feature_names": names,
|
||||
"groups": resolved_groups,
|
||||
}
|
||||
@@ -6,7 +6,7 @@ from typing import Tuple
|
||||
|
||||
from torchvision import transforms
|
||||
|
||||
from v4.classes.accessory.backbones import BACKBONES
|
||||
from v4.classes.accessory.backbones import BACKBONES, _is_timm_backbone
|
||||
|
||||
IMAGENET_MEAN: Tuple[float, float, float] = (0.485, 0.456, 0.406)
|
||||
IMAGENET_STD: Tuple[float, float, float] = (0.229, 0.224, 0.225)
|
||||
@@ -79,6 +79,11 @@ class ImageTransformConfig:
|
||||
def backbone_transform_config(backbone_name: str, augment: bool = True) -> ImageTransformConfig:
|
||||
"""Build an ImageTransformConfig using the backbone's default normalisation stats."""
|
||||
key = (backbone_name or "").lower()
|
||||
if _is_timm_backbone(key):
|
||||
# ConvNeXt-V2 and other timm models we currently expose are all
|
||||
# pretrained with standard ImageNet stats at 224×224.
|
||||
return ImageTransformConfig(crop_size=224, mean=IMAGENET_MEAN,
|
||||
std=IMAGENET_STD, augment=augment)
|
||||
if key not in BACKBONES:
|
||||
raise ValueError(f"Unknown backbone '{backbone_name}'.")
|
||||
spec = BACKBONES[key]
|
||||
|
||||
Reference in New Issue
Block a user