ML Learning Hub
رؤية الحاسوبمتقدم

تجزئة الصور: UNet وDeepLab

تصنيف كل بكسل — أقنعة دلالية وحدود المثيلات والفهم الشامل

التصنيف على مستوى البكسل — التجزئة الدلالية ومقابل الحالات في UNet وDeepLab وخسارة Dice لعدم توازن الفئات وتطبيقات التصوير الطبي والقيادة الذاتية.

40 min
9 مخططات
7 المفاهيم المغطاة

المتطلبات الأساسية

Object Detection

المفاهيم المغطاة

Semantic SegmentationInstance SegmentationUNetSkip ConnectionsDice LossmIoUDeepLab

الصيغ الرئيسية

خسارة Dice

تقيس التداخل بين الأقنعة المتنبأة والحقيقية — مقاومة لعدم توازن الفئات

mIoU

متوسط التقاطع على الاتحاد لكل فئة — مقياس التجزئة القياسي

اتصال التخطي

الاتصال المتبقي في UNet يدمج ميزات المشفّر مع ميزات المفكّك — يسترد التفاصيل المكانية المفقودة خلال التقليص

محاكاة تفاعلية

Loading visualization…
🎯

التجزئة: الفهم على مستوى البكسل

motivation

يُعطي الكشف عن الأجسام صناديق حدودية — تقريبات خشنة. تُعطي التجزئة أقنعة دقيقة على مستوى البكسل. يهم هذا لـ: التصوير الطبي (تحديد حدود ورم بدقة لتخطيط العلاج الإشعاعي)، القيادة الذاتية (التمييز بين السطح القابل للقيادة والرصيف عند كل بكسل)، صور الأقمار الاصطناعية (حساب مساحة المحاصيل بدقة م²)، وضع البورتريه (فصل الشخص عن الخلفية). ثلاثة مستويات: **التجزئة الدلالية** — وضع علامة على كل بكسل بفئة. **تجزئة المثيل** — كشف وإخفاء مثيلات أجسام فردية (Mask R-CNN). **التجزئة الشاملة** — مدمجة: أشياء (مثيلات) + مواد (مناطق غير محددة).

يستغرق الطبيب الشعاعي تحديد حدود الورم يدوياً 30-60 دقيقة لكل مسح. نموذج الذكاء الاصطناعي يفعل ذلك في < 1 ثانية — يتحول عنق الزجاجة إلى التحقق من المخرجات لا إنتاجها.

💡

هندسة المشفر-المفكك (UNet)

intuition

UNet هو بنية التجزئة النموذجية. المشفّر (المسار المنقبض) هو شبكة CNN تُقلّل خريطة الميزات تدريجياً — تتعلم 'ماذا' في الصورة وتفقد 'أين'. المفكّك (المسار الموسّع) يُوسّع تدريجياً إلى الدقة الأصلية باستخدام التلافيف المنقولة — يسترد 'أين'. الابتكار الرئيسي: اتصالات التخطي التي تدمج مباشرةً خرائط ميزات المشفّر مع نظيراتها في المفكّك. تُتيح هذه الاتصالات للنموذج الجمع بين الميزات الدلالية عالية المستوى مع التفاصيل المكانية منخفضة المستوى — أساسية للحدود الدقيقة والواضحة.

صُمِّمت UNet للتجزئة الطبية الحيوية عام 2015 مع صور تدريب قليلة جداً (~30). كفاءة البيانات تأتي من اتصالات التخطي وزيادة البيانات المكثفة.

</>

التجزئة مع torchvision وAlbumentations

code
python86 lines
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models.segmentation import DeepLabV3_ResNet101_Weights

class="tok-comment"># ── class="tok-num">1. Pretrained semantic segmentation (DeepLabV3) ───────────────────────────
model = models.segmentation.deeplabv3_resnet101(
    weights=DeepLabV3_ResNet101_Weights.DEFAULT
)
model.eval()

class="tok-comment"># Inference
from PIL import Image
import torchvision.transforms.functional as F
import numpy as np

img = Image.open(class="tok-str">"street.jpg").convert(class="tok-str">"RGB")
img_t = F.to_tensor(img).unsqueeze(class="tok-num">0)   class="tok-comment"># (class="tok-num">1, class="tok-num">3, H, W)
img_t = F.normalize(img_t, mean=[class="tok-num">0.485,class="tok-num">0.456,class="tok-num">0.406], std=[class="tok-num">0.229,class="tok-num">0.224,class="tok-num">0.225])

with torch.no_grad():
    output = model(img_t)[class="tok-str">"out"]         class="tok-comment"># (class="tok-num">1, class="tok-num">21, H, W) — class="tok-num">21 PASCAL VOC classes
pred_mask = output.argmax(dim=class="tok-num">1)[class="tok-num">0]     class="tok-comment"># (H, W) class labels

class="tok-comment"># ── class="tok-num">2. Minimal UNet ────────────────────────────────────────────────────────────
class DoubleConv(nn.Module):
    def __init__(self, in_ch, out_ch):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, class="tok-num">3, padding=class="tok-num">1, bias=False),
            nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),
            nn.Conv2d(out_ch, out_ch, class="tok-num">3, padding=class="tok-num">1, bias=False),
            nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),
        )
    def forward(self, x): return self.conv(x)

class UNet(nn.Module):
    def __init__(self, in_channels=class="tok-num">3, n_classes=class="tok-num">2, base_channels=class="tok-num">64):
        super().__init__()
        bc = base_channels
        class="tok-comment"># Encoder
        self.enc1 = DoubleConv(in_channels, bc)
        self.enc2 = DoubleConv(bc, bc*class="tok-num">2)
        self.enc3 = DoubleConv(bc*class="tok-num">2, bc*class="tok-num">4)
        self.pool = nn.MaxPool2d(class="tok-num">2)
        class="tok-comment"># Bottleneck
        self.bottleneck = DoubleConv(bc*class="tok-num">4, bc*class="tok-num">8)
        class="tok-comment"># Decoder
        self.up3  = nn.ConvTranspose2d(bc*class="tok-num">8, bc*class="tok-num">4, class="tok-num">2, stride=class="tok-num">2)
        self.dec3 = DoubleConv(bc*class="tok-num">8, bc*class="tok-num">4)   class="tok-comment"># bc*class="tok-num">8 because of skip connection
        self.up2  = nn.ConvTranspose2d(bc*class="tok-num">4, bc*class="tok-num">2, class="tok-num">2, stride=class="tok-num">2)
        self.dec2 = DoubleConv(bc*class="tok-num">4, bc*class="tok-num">2)
        self.up1  = nn.ConvTranspose2d(bc*class="tok-num">2, bc, class="tok-num">2, stride=class="tok-num">2)
        self.dec1 = DoubleConv(bc*class="tok-num">2, bc)
        class="tok-comment"># Output
        self.out  = nn.Conv2d(bc, n_classes, class="tok-num">1)

    def forward(self, x):
        e1 = self.enc1(x)
        e2 = self.enc2(self.pool(e1))
        e3 = self.enc3(self.pool(e2))
        b  = self.bottleneck(self.pool(e3))
        d3 = self.dec3(torch.cat([self.up3(b),  e3], dim=class="tok-num">1))
        d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=class="tok-num">1))
        d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=class="tok-num">1))
        return self.out(d1)

unet = UNet(n_classes=class="tok-num">2)
x = torch.randn(class="tok-num">2, class="tok-num">3, class="tok-num">256, class="tok-num">256)
out = unet(x)
print(fclass="tok-str">"UNet output: {out.shape}")   class="tok-comment"># (class="tok-num">2, class="tok-num">2, class="tok-num">256, class="tok-num">256)

class="tok-comment"># ── class="tok-num">3. Dice loss ──────────────────────────────────────────────────────────────
def dice_loss(pred, target, eps=class="tok-num">1e-6):
    class="tok-str">"""pred: (B, C, H, W) softmax, target: (B, H, W) long"""
    pred_soft = torch.softmax(pred, dim=class="tok-num">1)
    target_oh = torch.zeros_like(pred_soft)
    target_oh.scatter_(class="tok-num">1, target.unsqueeze(class="tok-num">1), class="tok-num">1)
    inter = (pred_soft * target_oh).sum(dim=(class="tok-num">2,class="tok-num">3))
    union = pred_soft.sum(dim=(class="tok-num">2,class="tok-num">3)) + target_oh.sum(dim=(class="tok-num">2,class="tok-num">3))
    return class="tok-num">1 - (class="tok-num">2*inter + eps) / (union + eps)

pred   = torch.randn(class="tok-num">2, class="tok-num">2, class="tok-num">64, class="tok-num">64)
target = torch.randint(class="tok-num">0, class="tok-num">2, (class="tok-num">2, class="tok-num">64, class="tok-num">64))
loss   = dice_loss(pred, target).mean()
print(fclass="tok-str">"Dice loss: {loss.item():.4f}")
⚠️

عدم توازن الفئات يدمر نماذج التجزئة

pitfall

في معظم مهام التجزئة تهيمن فئة الخلفية — قد تكون مشهد القيادة 95% سماء+طريق و5% مشاة. تعامل الإنتروبيا المتقاطعة جميع البكسلات بالتساوي فيتعلم النموذج التنبؤ بـ'خلفية' في كل مكان ويحصل على دقة 95% مع تفويت كل المشاة. الحلول: (1) الإنتروبيا المتقاطعة الموزونة — وزّن الخسارة عكسياً بتكرار الفئة. (2) خسارة Dice — غير حساسة طبيعياً للاختلال لأنها تقيس نسبة التقاطع. (3) Focal loss (من RetinaNet) — تُقلّل وزن البكسلات المصنّفة جيداً حتى يركز التدريب على الأمثلة الصعبة. من الناحية العملية الجمع بين الإنتروبيا المتقاطعة وخسارة Dice يعمل بشكل أفضل في التصوير الطبي.

تحقق دائماً من IoU لكل فئة في مقاييس التحقق — الدقة الإجمالية تُخفي الأداء السيئ على الفئات الصغيرة/النادرة التي غالباً ما تكون الأكثر أهمية.

?اختبار المعرفة

يتم حفظ التقدم في متصفحك — لا حاجة لحساب.

تحتاج مهندس ذكاء اصطناعي أو عالم بيانات؟

أبني نماذج تعلم آلي مخصصة، ووكلاء ذكاء اصطناعي، ورؤية حاسوب، وأتمتة — من الفكرة إلى الإنتاج.