File size: 3,676 Bytes
ae709c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""Standalone MobileNetV3-FPN string segmentation model."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import torch
from torch import nn
import torch.nn.functional as F


CHECKPOINT_FORMATS = {"yoyo_string_semantic_transfer_v1", "yoyo_string_semantic_unet_v1"}


def _group_count(channels: int) -> int:
    for groups in (8, 4, 2, 1):
        if channels % groups == 0:
            return groups
    return 1


class _FPNRefine(nn.Module):
    def __init__(self, channels: int):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Conv2d(channels, channels, 3, padding=1, groups=channels, bias=False),
            nn.GroupNorm(_group_count(channels), channels),
            nn.SiLU(inplace=True),
            nn.Conv2d(channels, channels, 1, bias=False),
            nn.GroupNorm(_group_count(channels), channels),
            nn.SiLU(inplace=True),
        )

    def forward(self, value: torch.Tensor) -> torch.Tensor:
        return self.layers(value)


class MobileNetV3FPNString(nn.Module):
    """MobileNetV3-Large encoder with a high-resolution FPN decoder."""

    _FEATURE_INDICES = (1, 3, 6, 12, 16)
    _FEATURE_CHANNELS = (16, 24, 40, 112, 960)

    def __init__(self, decoder_channels: int = 32):
        super().__init__()
        from torchvision.models import mobilenet_v3_large

        self.encoder = mobilenet_v3_large(weights=None).features
        self.lateral = nn.ModuleList(
            nn.Sequential(
                nn.Conv2d(input_channels, decoder_channels, 1, bias=False),
                nn.GroupNorm(_group_count(decoder_channels), decoder_channels),
            )
            for input_channels in self._FEATURE_CHANNELS
        )
        self.refine = nn.ModuleList(_FPNRefine(decoder_channels) for _ in range(4))
        self.classifier = nn.Sequential(
            _FPNRefine(decoder_channels),
            nn.Conv2d(decoder_channels, 1, 1),
        )

    def forward(self, value: torch.Tensor) -> torch.Tensor:
        input_size = value.shape[-2:]
        features = []
        selected = set(self._FEATURE_INDICES)
        for index, layer in enumerate(self.encoder):
            value = layer(value)
            if index in selected:
                features.append(value)
        pyramid = self.lateral[-1](features[-1])
        for level in range(len(features) - 2, -1, -1):
            pyramid = F.interpolate(pyramid, size=features[level].shape[-2:], mode="bilinear", align_corners=False)
            pyramid = self.refine[level](pyramid + self.lateral[level](features[level]))
        logits = self.classifier(pyramid)
        return F.interpolate(logits, size=input_size, mode="bilinear", align_corners=False)


def load_model(checkpoint: str | Path, device: str | torch.device = "cpu") -> tuple[nn.Module, dict[str, Any]]:
    """Load a StrandSeg-Lite checkpoint without importing the training repo."""
    target = torch.device(device)
    payload = torch.load(Path(checkpoint), map_location=target, weights_only=True)
    if not isinstance(payload, dict) or payload.get("format") not in CHECKPOINT_FORMATS:
        raise ValueError(f"Unsupported StrandSeg-Lite checkpoint: {checkpoint}")
    config = dict(payload.get("model_config") or {})
    architecture = str(config.get("architecture", "mobilenet_v3_fpn")).lower()
    if architecture != "mobilenet_v3_fpn":
        raise ValueError(f"This package supports mobilenet_v3_fpn, got {architecture!r}")
    model = MobileNetV3FPNString(decoder_channels=max(16, int(config.get("base_channels", 16)) * 2))
    model.load_state_dict(payload["state_dict"])
    model.to(target).eval()
    return model, payload