File size: 10,773 Bytes
55b2f87
 
 
 
 
 
 
 
 
 
 
 
 
870d5df
 
 
 
 
 
55b2f87
 
 
 
 
 
 
 
 
 
 
 
870d5df
 
55b2f87
 
 
 
 
 
 
 
 
 
 
 
 
bb56d04
 
55b2f87
 
 
 
 
 
 
 
870d5df
55b2f87
 
 
 
 
870d5df
55b2f87
 
 
 
 
 
870d5df
 
55b2f87
 
 
 
 
 
 
 
 
 
 
 
870d5df
 
 
55b2f87
 
 
 
 
 
870d5df
55b2f87
 
 
870d5df
 
 
 
55b2f87
 
 
 
 
 
 
 
 
 
870d5df
 
55b2f87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870d5df
 
 
 
 
55b2f87
 
 
 
 
 
 
870d5df
 
 
 
 
55b2f87
 
870d5df
 
 
55b2f87
870d5df
55b2f87
870d5df
 
55b2f87
870d5df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
94
95
96
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""Model factory: shared semantic + drywall-material heads over several backbones.

The original prototype hard-coded MobileNetV3-Large/DeepLabV3. That is small and
fast but well behind modern open segmentation models. This module keeps the
two-head design (semantic coverage + independent drywall substrate) but lets you
choose the encoder:

* ``deeplabv3_mobilenet_v3_large`` - the original; edge/latency oriented.
* ``deeplabv3_resnet50`` / ``deeplabv3_resnet101`` - stronger torchvision
  baselines (ASPP + ResNet), no extra dependencies.
* ``segformer_b0`` .. ``segformer_b5`` - modern transformer segmentation from
  Hugging Face (requires ``transformers``); the recommended accuracy option.

All variants return ``{"semantic": (B, C, H, W), "drywall": (B, M, H, W)}`` at
input resolution, where M is the number of material classes (3 with wallpaper;
2 for checkpoints trained before wallpaper existed), so ``train.py`` and ``predict.py`` are architecture-agnostic.
Models built with condition classes also return ``"condition": (B, K, H, W)`` -
surface condition (sound / paint failure / rust) from a third head on the same
features; checkpoints from before that head load without it.
Normalisation is shared (ImageNet mean/std), which SegFormer also expects.
"""
import torch
from torch import nn
from torch.nn import functional as F
from torchvision.models import MobileNet_V3_Large_Weights, ResNet50_Weights, ResNet101_Weights
from torchvision.models.segmentation import (
    deeplabv3_mobilenet_v3_large,
    deeplabv3_resnet50,
    deeplabv3_resnet101,
)

from label_schema import CONDITION_CLASSES, LEGACY_MATERIAL_CLASSES, MATERIAL_CLASSES

# torchvision DeepLabV3 variants: name -> (builder, pretrained backbone weights).
DEEPLAB_ARCHITECTURES = {
    "deeplabv3_mobilenet_v3_large": (deeplabv3_mobilenet_v3_large, MobileNet_V3_Large_Weights.DEFAULT),
    "deeplabv3_resnet50": (deeplabv3_resnet50, ResNet50_Weights.DEFAULT),
    "deeplabv3_resnet101": (deeplabv3_resnet101, ResNet101_Weights.DEFAULT),
}
# SegFormer variants: name -> Hugging Face id of a pretrained encoder/decoder.
SEGFORMER_ARCHITECTURES = {
    "segformer_b0": "nvidia/segformer-b0-finetuned-ade-512-512",
    "segformer_b1": "nvidia/segformer-b1-finetuned-ade-512-512",
    "segformer_b2": "nvidia/segformer-b2-finetuned-ade-512-512",
    "segformer_b3": "nvidia/segformer-b3-finetuned-ade-512-512",
    "segformer_b4": "nvidia/segformer-b4-finetuned-ade-512-512",
    # b5 has no 512-input ADE20K checkpoint on the Hub; the only b5 release is 640.
    "segformer_b5": "nvidia/segformer-b5-finetuned-ade-640-640",
}
DEFAULT_ARCH = "deeplabv3_mobilenet_v3_large"


def available_architectures():
    return sorted(list(DEEPLAB_ARCHITECTURES) + list(SEGFORMER_ARCHITECTURES))


def _drywall_head(channels, num_material_classes, hidden=128, dropout=0.1):
    return nn.Sequential(
        nn.Conv2d(channels, hidden, kernel_size=3, padding=1, bias=False),
        nn.BatchNorm2d(hidden),
        nn.ReLU(inplace=True),
        nn.Dropout2d(dropout),
        nn.Conv2d(hidden, num_material_classes, kernel_size=1),
    )


class WallPaintNet(nn.Module):
    """torchvision DeepLabV3 semantic head plus an independent drywall head."""

    def __init__(self, num_semantic_classes, pretrained_backbone=False, arch=DEFAULT_ARCH,
                 num_material_classes=len(MATERIAL_CLASSES), num_condition_classes=len(CONDITION_CLASSES)):
        super().__init__()
        if arch not in DEEPLAB_ARCHITECTURES:
            raise ValueError(f"unknown DeepLab architecture {arch!r}; choose from {sorted(DEEPLAB_ARCHITECTURES)}")
        builder, default_weights = DEEPLAB_ARCHITECTURES[arch]
        weights = default_weights if pretrained_backbone else None
        self.arch = arch
        self.segmenter = builder(weights=None, weights_backbone=weights, num_classes=num_semantic_classes)
        was_training = self.segmenter.training
        self.segmenter.eval()
        with torch.inference_mode():
            features = self.segmenter.backbone(torch.zeros(1, 3, 128, 128))["out"]
        self.segmenter.train(was_training)
        self.drywall_head = _drywall_head(features.shape[1], num_material_classes)
        self.condition_head = (_drywall_head(features.shape[1], num_condition_classes)
                               if num_condition_classes else None)

    def forward(self, x):
        size = x.shape[-2:]
        features = self.segmenter.backbone(x)
        semantic = self.segmenter.classifier(features["out"])
        drywall = self.drywall_head(features["out"])
        out = {
            "semantic": F.interpolate(semantic, size=size, mode="bilinear", align_corners=False),
            "drywall": F.interpolate(drywall, size=size, mode="bilinear", align_corners=False),
        }
        if self.condition_head is not None:
            out["condition"] = F.interpolate(self.condition_head(features["out"]), size=size, mode="bilinear",
                                             align_corners=False)
        return out


class SegformerPaintNet(nn.Module):
    """Hugging Face SegFormer semantic head plus an independent drywall head.

    SegFormer predicts logits at 1/4 resolution; the last encoder hidden state
    feeds the drywall head. Heavy enough to matter, but a genuine modern
    transformer backbone rather than a 2021 MobileNet.
    """

    def __init__(self, num_semantic_classes, pretrained_backbone=False, arch="segformer_b2",
                 num_material_classes=len(MATERIAL_CLASSES), num_condition_classes=len(CONDITION_CLASSES)):
        super().__init__()
        if arch not in SEGFORMER_ARCHITECTURES:
            raise ValueError(f"unknown SegFormer architecture {arch!r}; choose from {sorted(SEGFORMER_ARCHITECTURES)}")
        try:
            from transformers import SegformerConfig, SegformerForSemanticSegmentation
        except ImportError as exc:  # pragma: no cover - optional dependency
            raise ImportError("SegFormer architectures require `pip install transformers`") from exc
        hf_id = SEGFORMER_ARCHITECTURES[arch]
        self.arch = arch
        if pretrained_backbone:
            self.segmenter = SegformerForSemanticSegmentation.from_pretrained(
                hf_id, num_labels=num_semantic_classes, ignore_mismatched_sizes=True)
        else:
            config = SegformerConfig.from_pretrained(hf_id, num_labels=num_semantic_classes)
            self.segmenter = SegformerForSemanticSegmentation(config)
        self.drywall_head = _drywall_head(self.segmenter.config.hidden_sizes[-1], num_material_classes)
        # Peeling edges and rust streaks are fine detail: the condition head reads the
        # highest-resolution encoder stage (1/4 scale) rather than the coarsest one.
        self.condition_head = (_drywall_head(self.segmenter.config.hidden_sizes[0], num_condition_classes)
                               if num_condition_classes else None)

    def forward(self, x):
        size = x.shape[-2:]
        outputs = self.segmenter(pixel_values=x, output_hidden_states=True)
        semantic = F.interpolate(outputs.logits, size=size, mode="bilinear", align_corners=False)
        drywall = self.drywall_head(outputs.hidden_states[-1])
        drywall = F.interpolate(drywall, size=size, mode="bilinear", align_corners=False)
        out = {"semantic": semantic, "drywall": drywall}
        if self.condition_head is not None:
            out["condition"] = F.interpolate(self.condition_head(outputs.hidden_states[0]), size=size,
                                             mode="bilinear", align_corners=False)
        return out


def build_model(arch, num_semantic_classes, pretrained_backbone=False, num_material_classes=len(MATERIAL_CLASSES),
                num_condition_classes=len(CONDITION_CLASSES)):
    """Construct the model for ``arch`` (see :func:`available_architectures`); 0 condition classes = no condition head."""
    if arch in DEEPLAB_ARCHITECTURES:
        return WallPaintNet(num_semantic_classes, pretrained_backbone, arch, num_material_classes, num_condition_classes)
    if arch in SEGFORMER_ARCHITECTURES:
        return SegformerPaintNet(num_semantic_classes, pretrained_backbone, arch, num_material_classes,
                                 num_condition_classes)
    raise ValueError(f"unknown architecture {arch!r}; choose from {available_architectures()}")


def checkpoint_material_classes(checkpoint):
    """Material classes a checkpoint was trained with (pre-wallpaper checkpoints have two)."""
    names = tuple(checkpoint.get("material_classes") or ())
    if names not in (MATERIAL_CLASSES, LEGACY_MATERIAL_CLASSES):
        raise ValueError(f"checkpoint material classes {names!r} are not a known schema; retrain with drywall_masks")
    return names


def checkpoint_condition_classes(checkpoint):
    """Condition classes a checkpoint was trained with; () for checkpoints from before the condition head."""
    names = tuple(checkpoint.get("condition_classes") or ())
    if names not in ((), CONDITION_CLASSES):
        raise ValueError(f"checkpoint condition classes {names!r} are not a known schema")
    return names


def build_from_checkpoint(checkpoint, num_semantic_classes):
    """Rebuild and load the model a checkpoint describes: 2 or 3 materials, with or without a condition head."""
    model = build_model(str(checkpoint.get("arch", DEFAULT_ARCH)), num_semantic_classes, pretrained_backbone=False,
                        num_material_classes=len(checkpoint_material_classes(checkpoint)),
                        num_condition_classes=len(checkpoint_condition_classes(checkpoint)))
    model.load_state_dict(checkpoint["model"])
    return model


def warm_start(model, checkpoint):
    """Initialise ``model`` from a checkpoint of the same arch, growing the material head if needed.

    A pre-wallpaper checkpoint has a 2-way material classifier; its rows are
    copied into the new 3-way head and the wallpaper row keeps its fresh init,
    so facade/wall knowledge survives while wallpaper is learned. A checkpoint
    without a condition head leaves that head at its fresh init. Returns the
    names of tensors that could not be copied (empty when everything matched).
    """
    target = model.state_dict()
    skipped = []
    for name, value in checkpoint["model"].items():
        if name not in target:
            skipped.append(name)
        elif target[name].shape == value.shape:
            target[name] = value
        elif name.startswith("drywall_head.") and value.shape[1:] == target[name].shape[1:] \
                and value.shape[0] < target[name].shape[0]:
            target[name][:value.shape[0]] = value
        else:
            skipped.append(name)
    model.load_state_dict(target)
    return skipped