Detectarea obiectelor vs segmentare: comparație și cazuri de utilizare
Atunci când abordați o problemă de viziune computerizată, alegeți sarcina și arhitectura potrivite și fundamentale. Detectarea obiectelor, Segmentarea semantică, Segmentarea instanțelor e Segmentarea panoptică nu sunt alternative interschimbabile: fiecare răspunde la întrebări diferite, are cerințe de calcul diferite și se pretează la cazuri de utilizare specifice. Alegerea unei abordări greșite înseamnă irosirea resurselor sau, mai rău, nu rezolvă problema clientului.
În acest articol vom face o comparație riguroasă între principalele sarcini de viziune vizuală pe computer, cu implementări practice în PyTorch și linii directoare concrete pentru alegerea abordării potrivite în proiectul tău.
Ce vei învăța
- Diferențele fundamentale dintre detecție, semantică, instanță și segmentare panoptică
- Când să folosiți ce abordare: arbore decizional practic
- Arhitecturile principale pentru fiecare sarcină și compromisurile acestora
- Implementarea completă a unei conducte multi-task în PyTorch
- Valori de evaluare pentru fiecare sarcină (mAP, mIoU, PQ)
- Benchmark de viteză și acuratețe pe hardware real
- Studii de caz: vehicule autonome, supraveghere medicală, analize de retail
1. Sarcinile principale ale Computer Vision
Înainte de a compara abordările, să definim cu precizie fiecare sarcină cu exemple vizuale:
Immagine input: una strada con 3 persone e 2 auto
┌─────────────────────────────────────────────────────────────────┐
│ IMAGE CLASSIFICATION: "strada con veicoli e persone" │
│ Output: 1 label per tutta l'immagine │
│ Non dice WHERE ne QUANTI oggetti ci sono │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ OBJECT DETECTION: 5 bounding boxes │
│ [persona(0.95) x1,y1,x2,y2] │
│ [persona(0.88) x1,y1,x2,y2] │
│ [persona(0.91) x1,y1,x2,y2] │
│ [auto(0.97) x1,y1,x2,y2] │
│ [auto(0.94) x1,y1,x2,y2] │
│ Sa WHERE e QUANTI, ma non la forma precisa │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SEGMENTAZIONE SEMANTICA: ogni pixel ha una classe │
│ pixel(100,200)="persona", pixel(300,400)="auto" │
│ Sa la FORMA precisa, ma non distingue le istanze │
│ Tutte le "persone" = stessa categoria, non identità separate │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SEGMENTAZIONE DI ISTANZA: maschera per ogni oggetto │
│ persona_1 = {pixel: (100,200),(101,200),...} │
│ persona_2 = {pixel: (250,180),(251,180),...} │
│ Sa la FORMA e distingue le ISTANZE separate │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SEGMENTAZIONE PANOPTICA: unione di semantica + istanza │
│ "cose" (countable): istanza per persona e auto │
│ "stuff" (uncountable): semantica per strada, cielo, edifici │
│ Sa TUTTO: forma, classe, istanza, sfondo │
└─────────────────────────────────────────────────────────────────┘
1.1 Comparație tehnică detaliată
Comparația sarcinilor de viziune computerizată
| Sarcini | Ieșiri | Complexitate | Viteză | memorie GPU | Metric |
|---|---|---|---|---|---|
| Clasificare | Etichetă + prob | Scăzut | Foarte sus | Scăzut | Top-1/5 Acc |
| Detectarea obiectelor | BBox + etichetă | Medie | Ridicat | Medie | mAP@0,5 |
| Sec. Semantică | Hartă cu etichetă de pixeli | Mediu-Ridicat | Medie | Ridicat | mIoU |
| Sec. Exemplu | BBox + masca | Ridicat | Scăzut-Mediu | Ridicat | mAP@masca |
| Sec. Panoptic | Toate | Foarte sus | Scăzut | Foarte sus | PQ |
2. Detectarea obiectelor: arhitecturi și implementare
2.1 Într-o singură etapă vs. în două etape
Detectoarele de obiecte sunt împărțite în două mari categorii arhitecturale:
Detectoare cu o singură etapă vs Detectoare cu două etape
| Caracteristică | O singură etapă (YOLO, SSD, RetinaNet) | În două etape (R-CNN mai rapid, Mască R-CNN) |
|---|---|---|
| Conducte | Rețea unică, predicție directă | RPN propune regiuni, apoi clasificare |
| Viteză | Ridicat (30-150+ FPS) | Scăzut (5-15 FPS) |
| Precizie | Ușor mai jos pe obiectele mici | Precizie mai bună, în special obiectele mici |
| Utilizare tipică | În timp real, margine, video | Analiză offline, precizie maximă |
| Exemple moderne | YOLO26, RT-DETR, DINO-DETR | Faster R-CNN, Cascade R-CNN, DETR |
import torch
import torchvision
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection import FasterRCNN_ResNet50_FPN_Weights
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
def create_faster_rcnn(num_classes: int) -> torch.nn.Module:
"""
Faster R-CNN con backbone ResNet-50 + FPN pre-addestrato.
Two-stage: RPN (Region Proposal Network) + classificatore.
"""
# Carica con pesi COCO pre-addestrati
model = fasterrcnn_resnet50_fpn(
weights=FasterRCNN_ResNet50_FPN_Weights.DEFAULT
)
# Sostituisce il classificatore per il numero di classi custom
# +1 perchè la classe 0 e riservata al "background"
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes + 1)
return model
def train_detection_model(model, data_loader, num_epochs: int = 10, lr: float = 0.005):
"""
Training loop per Faster R-CNN.
Il modello calcola automaticamente le loss interne (classification + bbox regression + RPN).
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
model.train()
optimizer = torch.optim.SGD(
model.parameters(),
lr=lr,
momentum=0.9,
weight_decay=0.0005
)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
for epoch in range(num_epochs):
total_loss = 0.0
for images, targets in data_loader:
images = [img.to(device) for img in images]
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
# Faster R-CNN restituisce un dizionario di loss in training mode
loss_dict = model(images, targets)
losses = sum(loss for loss in loss_dict.values())
optimizer.zero_grad()
losses.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += losses.item()
scheduler.step()
avg_loss = total_loss / len(data_loader)
print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f}")
def inference_faster_rcnn(model, image_tensor: torch.Tensor,
score_threshold: float = 0.5) -> list[dict]:
"""Inference con Faster R-CNN - restituisce predizioni filtrate."""
device = next(model.parameters()).device
model.eval()
with torch.no_grad():
predictions = model([image_tensor.to(device)])
results = []
pred = predictions[0]
for i, score in enumerate(pred['scores']):
if score >= score_threshold:
results.append({
'bbox': pred['boxes'][i].tolist(),
'score': float(score),
'label': int(pred['labels'][i])
})
return results
3. Segmentarea semantică
La segmentare semantică atribuie o etichetă de clasă fiecărui pixel individual a imaginii. Nu distinge cazurile: toți „oamenii” aparțin aceleiași clase. Este ideal pentru analiza completă a scenei (conducere autonomă, analiză medicală, teledetecție).
3.1 DeepLabv3: Convoluții atroce
DeepLabv3 (Chen et al., 2017) folosește circumvoluții atroce (sau dilatat circumvoluții): circumvoluții cu „găuri” care măresc câmpul receptiv fără a crește parametrii, esențial pentru captarea contextului multi-scală fără a reduce rezoluția.
import torch
import torch.nn as nn
import torchvision.models.segmentation as seg_models
from torchvision.models.segmentation import DeepLabV3_ResNet50_Weights
def create_deeplabv3(num_classes: int) -> nn.Module:
"""
DeepLabv3 con backbone ResNet-50 pre-addestrato su COCO.
Usa Atrous Spatial Pyramid Pooling (ASPP) per multi-scale context.
"""
model = seg_models.deeplabv3_resnet50(
weights=DeepLabV3_ResNet50_Weights.DEFAULT
)
# Sostituisce il classificatore finale per il numero di classi custom
model.classifier[-1] = nn.Conv2d(
in_channels=256,
out_channels=num_classes,
kernel_size=1
)
# Anche l'auxiliary classifier (per training stability)
model.aux_classifier[-1] = nn.Conv2d(
in_channels=256,
out_channels=num_classes,
kernel_size=1
)
return model
def train_semantic_segmentation(model, data_loader, num_epochs: int = 20):
"""
Training loop per segmentazione semantica.
Loss: CrossEntropyLoss (ignora label -1 per pixel non annotati)
"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
criterion = nn.CrossEntropyLoss(ignore_index=255) # 255 = unlabeled pixel
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.PolynomialLR(optimizer, total_iters=num_epochs)
for epoch in range(num_epochs):
model.train()
total_loss = 0.0
for images, masks in data_loader:
images = images.to(device)
masks = masks.long().to(device) # [B, H, W] con valori 0..num_classes-1
# DeepLabv3 restituisce dict con 'out' e 'aux'
outputs = model(images)
main_loss = criterion(outputs['out'], masks)
aux_loss = criterion(outputs['aux'], masks) * 0.4 # peso ridotto
loss = main_loss + aux_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
scheduler.step()
avg_loss = total_loss / len(data_loader)
miou = compute_miou(model, data_loader, device)
print(f"Epoch {epoch+1}/{num_epochs} | Loss: {avg_loss:.4f} | mIoU: {miou:.3f}")
def compute_miou(model, data_loader, device, num_classes: int = 21) -> float:
"""Calcola Mean IoU (metrica standard per segmentazione semantica)."""
model.eval()
intersection = torch.zeros(num_classes, device=device)
union = torch.zeros(num_classes, device=device)
with torch.no_grad():
for images, masks in data_loader:
images = images.to(device)
masks = masks.long().to(device)
preds = model(images)['out'].argmax(dim=1) # [B, H, W]
for cls in range(num_classes):
pred_cls = preds == cls
true_cls = masks == cls
intersection[cls] += (pred_cls & true_cls).sum()
union[cls] += (pred_cls | true_cls).sum()
iou = intersection / (union + 1e-10)
return float(iou[union > 0].mean())
4. Segmentarea instanțelor cu Masca R-CNN
La segmentarea instanțelor combinați detectarea obiectelor (caseta de delimitare + clasă) cu segmentare la nivel de pixeli pentru fiecare instanță individuală. Fiecare obiect are propria sa masca binar independent. Mască R-CNN (He et al., 2017) extinde Faster R-CNN adăugând un al treilea „cap” paralel pentru predicția măștii.
import torch
import torchvision
from torchvision.models.detection import maskrcnn_resnet50_fpn
from torchvision.models.detection import MaskRCNN_ResNet50_FPN_Weights
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
def create_mask_rcnn(num_classes: int) -> torch.nn.Module:
"""
Mask R-CNN: Faster R-CNN + Mask Head.
Output per ogni istanza: bbox + classe + maschera binaria 28x28.
"""
model = maskrcnn_resnet50_fpn(
weights=MaskRCNN_ResNet50_FPN_Weights.DEFAULT
)
# Sostituisce box predictor
in_features_box = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(
in_features_box, num_classes + 1
)
# Sostituisce mask predictor
in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels
hidden_layer = 256
model.roi_heads.mask_predictor = MaskRCNNPredictor(
in_features_mask, hidden_layer, num_classes + 1
)
return model
def prepare_instance_target(boxes: list, labels: list, masks: list) -> dict:
"""
Prepara il target nel formato richiesto da Mask R-CNN.
masks: lista di array booleani [H, W] per ogni istanza.
"""
return {
'boxes': torch.tensor(boxes, dtype=torch.float32),
'labels': torch.tensor(labels, dtype=torch.int64),
'masks': torch.tensor(masks, dtype=torch.uint8) # [N, H, W]
}
def visualize_instance_predictions(image, predictions, score_threshold: float = 0.5):
"""
Visualizza bounding boxes e maschere di istanza su un'immagine.
"""
import numpy as np
import cv2
img = np.array(image)
colors = [(np.random.randint(100, 255), np.random.randint(100, 255),
np.random.randint(100, 255)) for _ in range(100)]
pred = predictions[0]
valid_idx = pred['scores'] >= score_threshold
for i, (box, mask, score, label) in enumerate(zip(
pred['boxes'][valid_idx],
pred['masks'][valid_idx],
pred['scores'][valid_idx],
pred['labels'][valid_idx]
)):
color = colors[i % len(colors)]
# Disegna bounding box
x1, y1, x2, y2 = [int(c) for c in box]
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
# Applica maschera semitrasparente
mask_binary = (mask[0].numpy() > 0.5).astype(np.uint8)
overlay = img.copy()
overlay[mask_binary == 1] = color
img = cv2.addWeighted(img, 0.6, overlay, 0.4, 0)
# Label con confidence
text = f"class {int(label)}: {float(score):.2f}"
cv2.putText(img, text, (x1, y1-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
return img
5. Arborele de decizie: ce sarcină să alegeți?
Problema: "Cosa voglio sapere dell'immagine?"
|
├─ Solo "che oggetti ci sono"?
│ └── IMAGE CLASSIFICATION
│ Architetture: ResNet, EfficientNet, ViT
│ Esempi: quality gate industriale, filtro contenuti
│
├─ "Dove sono gli oggetti + quanti sono"?
│ └── OBJECT DETECTION
│ │
│ ├─ Serve velocità real-time (>30 FPS)?
│ │ └── Single-Stage: YOLO26, RT-DETR
│ │
│ └─ Serve massima accuratezza (oggetti piccoli)?
│ └── Two-Stage: Faster R-CNN, DETR
│
├─ "Che classe e ogni pixel" (no distinzione istanze)?
│ └── SEGMENTAZIONE SEMANTICA
│ Architetture: DeepLabv3, FCN, SegFormer
│ Esempi: analisi stradale, medica, telerilevamento
│
├─ "Separare ogni oggetto + sua forma esatta"?
│ └── SEGMENTAZIONE DI ISTANZA
│ Architetture: Mask R-CNN, SOLOv2, YOLACT
│ Esempi: conteggio oggetti, robotica, biologia
│
└─ "Tutto: oggetti separati + sfondo classificato"?
└── SEGMENTAZIONE PANOPTICA
Architetture: Panoptic FPN, Mask2Former
Esempi: guida autonoma completa, scene understanding
Cazuri de utilizare pentru fiecare sarcină
| Sector | Detectare | Sec. Semantică | Sec. Exemplu | Panoptic |
|---|---|---|---|---|
| Automobile | Detectarea pietonilor/vehiculelor | Segmentează drum/bandă | Separați fiecare pion | Scena completă de sine stătătoare |
| Doctor | Localizați leziunile pe CT | Organe segmentare | Separați fiecare tumoră | Analiza anatomică completă |
| Cu amănuntul | Produse de rafturi | Harta planogramei | Identificați fiecare produs | Analiza completă a raftului |
| Industrial | Detectarea defectelor (caseta de delimitare) | Clasificarea zonelor defectuoase | Segmentați fiecare defect | Inspecție completă a piesei |
| Agricultură | Numărați fructele pe copac | Segmentează vegetația | Separați fiecare fruct | Harta completă a câmpului |
6. Conductă Multi-Task: Detectare + Segmentare
În multe aplicații din lumea reală, este convenabil să combinați mai multe sarcini într-o singură arhitectură pentru eficiență de calcul. Un exemplu practic: în analiza retailului dorim să localizăm atât produsele (detecție) decât segmentarea zonei ocupate pe raft (segmentare semantică).
import torch
import torch.nn as nn
import torchvision.models as models
class MultiTaskDetectionSegmentation(nn.Module):
"""
Architettura multi-task che condivide un backbone ResNet-50 + FPN
tra due head: detection e segmentazione semantica.
"""
def __init__(self, num_det_classes: int, num_seg_classes: int):
super().__init__()
# Backbone condiviso: ResNet-50 con FPN
backbone = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
# Estrae feature a più scale
self.layer1 = nn.Sequential(backbone.conv1, backbone.bn1,
backbone.relu, backbone.maxpool,
backbone.layer1) # 1/4 risoluzione
self.layer2 = backbone.layer2 # 1/8
self.layer3 = backbone.layer3 # 1/16
self.layer4 = backbone.layer4 # 1/32
# FPN (Feature Pyramid Network) per multi-scale features
self.fpn = nn.ModuleDict({
'p5': nn.Conv2d(2048, 256, 1),
'p4': nn.Conv2d(1024, 256, 1),
'p3': nn.Conv2d(512, 256, 1),
'p2': nn.Conv2d(256, 256, 1),
})
# Detection head (semplificato)
self.det_head = nn.Sequential(
nn.Conv2d(256, 256, 3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, num_det_classes * (4 + 1), 1)
# 4 bbox coords + 1 objectness per ogni classe
)
# Segmentation head (decoder con upsampling)
self.seg_head = nn.Sequential(
nn.Conv2d(256, 256, 3, padding=1),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(256, 128, 4, stride=2, padding=1), # 2x upsample
nn.ReLU(inplace=True),
nn.ConvTranspose2d(128, 64, 4, stride=2, padding=1), # 4x upsample
nn.ReLU(inplace=True),
nn.Conv2d(64, num_seg_classes, 1)
)
def forward(self, x: torch.Tensor) -> dict:
# Backbone
c2 = self.layer1(x) # 1/4
c3 = self.layer2(c2) # 1/8
c4 = self.layer3(c3) # 1/16
c5 = self.layer4(c4) # 1/32
# FPN top-down pathway
p5 = self.fpn['p5'](c5)
p4 = self.fpn['p4'](c4) + nn.functional.interpolate(p5, scale_factor=2)
p3 = self.fpn['p3'](c3) + nn.functional.interpolate(p4, scale_factor=2)
p2 = self.fpn['p2'](c2) + nn.functional.interpolate(p3, scale_factor=2)
# Task-specific heads
det_output = self.det_head(p3) # detection sul livello P3
seg_output = self.seg_head(p2) # segmentation su P2 (più alta risoluzione)
# Upsample seg output a dimensione input
seg_output = nn.functional.interpolate(
seg_output, size=x.shape[-2:], mode='bilinear', align_corners=False
)
return {'detection': det_output, 'segmentation': seg_output}
def compute_multitask_loss(outputs: dict, det_targets, seg_targets) -> torch.Tensor:
"""
Loss combinata multi-task con pesi bilanciati.
Loss totale = w_det * L_det + w_seg * L_seg
"""
det_criterion = nn.BCEWithLogitsLoss()
seg_criterion = nn.CrossEntropyLoss(ignore_index=255)
det_loss = det_criterion(outputs['detection'], det_targets)
seg_loss = seg_criterion(outputs['segmentation'], seg_targets)
# Pesi relativi (da tuning sperimentale)
total_loss = 1.0 * det_loss + 0.5 * seg_loss
return total_loss, {'det': det_loss.item(), 'seg': seg_loss.item()}
7. Cele mai bune practici și compararea performanței
Performanță de referință pe setul de date COCO (2025)
| Model | Sarcini | HARTĂ/mIoU | FPS (V100) | Params |
|---|---|---|---|---|
| YOLO26m | Detectare | 57,2 mAP | 100+ | 25M |
| R-CNN R50 mai rapid | Detectare | 40,2 mAP | 18 | 41M |
| DeepLabv3 R50 | Sec. Semantică | 74,3 mil | 45 | 39M |
| SegFormer-B5 | Sec. Semantică | 83,1 mil | 15 | 85 de milioane |
| Mască R-CNN R50 | Sec. Exemplu | 36,1 mAP | 14 | 44M |
| Mask2Former R50 | Panoptic | 51,9 PQ | 8 | 44M |
Greșeli comune de proiectare
- Utilizați segmentarea când detectarea este suficientă: Dacă trebuie doar să numărați sau să localizați obiecte, utilizați detectarea. Segmentarea este mult mai costisitoare de adnotat și antrenat.
- Ignorați cerințele în timp real: Masca R-CNN la 14 FPS nu este acceptabilă pentru un sistem de supraveghere live. Alegeți-vă arhitectura în funcție de cerințele dvs. de latență.
- Set de date dezechilibrat pentru segmentare: Dacă o clasă ocupă 95% din pixeli (de exemplu, fundal), modelul o va învăța trivial. Utilizați pierderea ponderată sau eșantionarea în clasă.
- Confuză mIoU și map: Sunt metrici diferite. mIoU măsoară precizia pixel cu pixel (segmentare), mAP măsoară calitatea casetelor de delimitare (detecție).
- Multi-sarcină fără echilibrare: În arhitecturile multi-task, pierderile diferitelor sarcini pot avea scări foarte diferite. Utilizați normalizarea gradientului sau ponderarea incertitudinii.
Concluzii
Am explorat întregul spectru de sarcini de viziune computerizată, din diferențele lor fundamentale la implementări practice:
- Clasificare, Detectare, Semantică, Instanță și Segmentare Panoptică au rezultate, costuri și cazuri de utilizare diferite
- YOLO26 este regele detectării în timp real; R-CNN mai rapid excelează în acuratețea offline
- DeepLabv3 este excelent pentru segmentarea semantică; Masca R-CNN adaugă distincție de instanță
- Arhitecturile multi-task vă permit să combinați mai multe sarcini cu o coloană vertebrală partajată
- Arborele de decizie prezentat ghidează alegerea abordării potrivite pentru fiecare problemă







