Image Segmentation
PyTorch
English
painting-vision-robotics-kit
semantic-segmentation
robotics
edge-ai
construction-ai
autonomous-painting
wall-painting-robot
paint-coverage-estimation
building-facade
drywall
skirting-detection
window-detection
lidar
depth-validation
segformer
deeplabv3
Eval Results (legacy)
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
|