در پردازش تصویر برای مهندسی برق، یک امتیاز خوب همیشه به معنی درست بودن نتیجه برای وظیفهٔ نهایی نیست. ممکن است ماسکِ مسیر یک برد تقریباً با مرجع هم‌پوشانی داشته باشد، اما یک شکاف کوچک، پیوستگی الگو را عوض کند. این آموزش با یک ماسک مصنوعی و کد کامل پایتون نشان می‌دهد چگونه خودِ محاسبهٔ IoU و Dice را آزمون کنیم، حالت‌های مرزی را آشکار نگه داریم و از تفسیر بیش از اندازهٔ یک عدد جلوگیری کنیم. این مثال سامانهٔ بازرسی برد یا خروجی یک مدل آموزش‌دیده نیست.

۱. مرجع، پیش‌زمینه و قرارداد ورودی را تعریف کنید

قرارداد پیش‌زمینهٔ سفید و پس‌زمینهٔ سیاه در دو ماسک هم‌جهت
تصویر مفهومی: قرارداد پیش‌زمینهٔ سفید و پس‌زمینهٔ سیاه در دو ماسک هم‌جهت؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

یک آرایهٔ ۱۲۸ در ۱۲۸ پیکسلی می‌سازیم که بخشی از آن به شکل حرف L فعال است؛ شکل فقط یادآور مسیر رسانای برد است و از تصویر واقعی قطعه استخراج نشده است. مقدار درست یا True، پیش‌زمینه و مقدار نادرست یا False، پس‌زمینه است. مرجع ۱۱۵۲ پیکسل فعال دارد. سه خروجی مقایسه‌ای می‌سازیم: تطابق کامل، جابه‌جایی دو ستون و حذف شکافی کوچک. خروجی تماماً پس‌زمینه و جفت ماسکِ بدون پیش‌زمینه هم برای آزمون مرزی اضافه می‌شوند.

دو ماسک باید هم‌اندازه، دوبعدی و از نوع بولی باشند. تابع ما آرایهٔ عدد صحیح یا احتمال را خودکار به بولی تبدیل نمی‌کند، چون هر مقدار غیرصفر الزاماً تصمیم درستِ پیش‌زمینه نیست. اگر مدل احتمال تولید می‌کند، تبدیل آن به ماسک به یک آستانهٔ تعریف‌شده نیاز دارد؛ انتخاب آن آستانه بخشی از این مثال نیست. اندازهٔ فیزیکی هر پیکسل نیز معلوم نیست، پس مساحت را با شمار پیکسل گزارش می‌کنیم و از میلی‌متر حرف نمی‌زنیم.

در کاربرد واقعی، هم‌اندازه بودن برای هم‌ترازی کافی نیست. چرخش دوربین، جابه‌جایی برد یا تغییر بزرگ‌نمایی می‌تواند دو ماسک را روی شبکه‌های مکانی متفاوت قرار دهد. پیش از مقایسه، باید ناحیهٔ بررسی، جهت تصویر و روش تبدیل هندسی روشن باشند. اینجا همهٔ آرایه‌ها روی یک شبکهٔ مشخص ساخته شده‌اند و جابه‌جایی، تغییر عمدیِ خروجی آزمایش است؛ هیچ ثبت تصویر یا اصلاح دوربینی انجام نشده است.

۲. هم‌پوشانی را از دقت پیکسلی جدا کنید

تعریف IoU و Dice با شمارش TP و FP و FN
تصویر مفهومی: تعریف IoU و Dice با شمارش TP و FP و FN؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

TP شمار پیکسل‌هایی است که هم در مرجع و هم در خروجی فعال‌اند. FP پیکسلِ فعالِ اضافی در خروجی و FN پیکسلِ فعالِ ازدست‌رفتهٔ مرجع است. طبق تعریف Jaccard در مستندات MathWorks، IoU از تقسیم TP بر مجموع TP و FP و FN به دست می‌آید. مقدار آن برای اجتماع غیرخالی بین صفر و یک است و دربارهٔ هم‌پوشانی پیش‌زمینه صحبت می‌کند.

معیار Dice دو برابر TP را بر مجموع دو برابر TP و FP و FN تقسیم می‌کند. برای همین قرارداد و اجتماع غیرخالی، رابطهٔ Dice = 2*IoU/(1+IoU) برقرار است. بنابراین دو عدد مستقل دربارهٔ دو ویژگی متفاوتِ هندسی نداریم؛ Dice بزرگ‌تر، به تنهایی نشانهٔ برتری یک روش نسبت به IoU نیست. هنگام مقایسهٔ گزارش‌ها، نام معیار و قرارداد آن باید یکسان باشند.

دقت پیکسلی، نسبت همهٔ پیکسل‌های درست، شامل پس‌زمینه، به کل پیکسل‌هاست. در آرایهٔ ما پس‌زمینه بخش بزرگی از تصویر را می‌گیرد. اگر خروجی همیشه پس‌زمینه باشد، دقت پیکسلی حدود ۹۲٫۹۷٪ می‌شود، در حالی که هیچ پیکسل مسیر پیدا نشده و IoU و Dice صفرند. این تفاوت از تعریف معیارها می‌آید؛ عدد ۹۳٪ در چنین گزارشی نباید به عنوان موفقیت در استخراج مسیر معرفی شود.

۳. کد کامل و آزمون ویژگی‌های معیار

آزمون همانی، تقارن و مقایسهٔ مستقل معیار ماسک
تصویر مفهومی: آزمون همانی، تقارن و مقایسهٔ مستقل معیار ماسک؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

کد زیر فقط ماسک‌های ساخته‌شده در برنامه را بررسی می‌کند. آزمون همانی می‌گوید ماسک غیرخالی با خودش باید امتیاز یک بگیرد. آزمون تقارن می‌گوید جابه‌جایی نقش دو ماسک، IoU و Dice را تغییر نمی‌دهد. جمع چهار شمارش TP، FP، FN و TN نیز باید تعداد کل پیکسل‌ها شود. برای اجتماع‌های غیرخالی، نتیجه با jaccard_score در scikit-learn بعد از تبدیل ماسک‌ها به بردار مقایسه می‌شود.

برای جفت ماسکِ بدون پیش‌زمینه، مخرج معیار صفر است. سیاست این مثال، مقدار None و ثبت جداگانهٔ این حالت است؛ آن را بی‌صدا امتیاز کاملِ کشف پیش‌زمینه حساب نمی‌کنیم. این تصمیم باید هنگام تجمیع نتایج هم حفظ شود و تعداد جفت‌های خالی اعلام شود. کتابخانه‌ها ممکن است سیاست دیگری داشته باشند؛ یکسان بودن نام تابع جای بررسی قرارداد تقسیم بر صفر را نمی‌گیرد.

"""Synthetic 2-D mask fixtures, not a PCB inspection system or trained model."""
import hashlib
import json
import math
import platform
import numpy as np
import scipy
import sklearn
from scipy.ndimage import label
from sklearn.metrics import jaccard_score


def mask_scores(reference, prediction):
    a, b = np.asarray(reference), np.asarray(prediction)
    if a.dtype != np.bool_ or b.dtype != np.bool_:
        raise ValueError("Explicit bool masks required; threshold outside this function")
    if a.ndim != 2 or a.shape != b.shape or a.size == 0:
        raise ValueError("Same nonempty two-dimensional shape required")
    tp = int(np.count_nonzero(a & b))
    fp = int(np.count_nonzero(~a & b))
    fn = int(np.count_nonzero(a & ~b))
    tn = int(np.count_nonzero(~a & ~b))
    union = tp + fp + fn
    foreground_sum = 2*tp + fp + fn
    return {"tp": tp, "fp": fp, "fn": fn, "tn": tn,
            "iou": tp/union if union else None,
            "dice": 2*tp/foreground_sum if foreground_sum else None,
            "pixel_accuracy": (tp+tn)/a.size,
            "empty_foreground_pair": union == 0}


truth = np.zeros((128, 128), dtype=bool)
truth[40:48, 16:112] = True
truth[40:96, 104:112] = True
shifted = np.zeros_like(truth)
shifted[:, 2:] = truth[:, :-2]  # Translation, without wrap-around.
broken = truth.copy()
broken[40:48, 60:62] = False
empty = np.zeros_like(truth)
cases = {"perfect": (truth, truth.copy()),
         "shifted_two_columns": (truth, shifted),
         "broken_connection": (truth, broken),
         "all_background": (truth, empty),
         "empty_both": (empty, empty)}
results = {name: mask_scores(a, b) for name, (a, b) in cases.items()}
checks = {}
assert results["perfect"]["iou"] == results["perfect"]["dice"] == 1.0
checks["identity"] = True
for a, b in cases.values():
    forward, reverse = mask_scores(a, b), mask_scores(b, a)
    assert forward["iou"] == reverse["iou"]
    assert forward["dice"] == reverse["dice"]
checks["symmetric_overlap_scores"] = True
for result in results.values():
    assert sum(result[k] for k in ("tp", "fp", "fn", "tn")) == truth.size
    if result["iou"] is not None:
        assert math.isclose(result["dice"], 2*result["iou"]/(1+result["iou"]))
checks["count_conservation_and_metric_relation"] = True
for a, b in cases.values():
    own = mask_scores(a, b)["iou"]
    if own is not None:
        reference_score = float(jaccard_score(a.ravel(), b.ravel(),
                                              average="binary", pos_label=True))
        assert math.isclose(own, reference_score, abs_tol=1e-15)
checks["nonempty_iou_matches_sklearn"] = True
assert results["empty_both"]["iou"] is None
assert results["empty_both"]["dice"] is None
checks["empty_pair_policy_explicit"] = True
assert results["all_background"]["pixel_accuracy"] > 0.9
assert results["all_background"]["iou"] == 0.0
checks["background_accuracy_trap"] = True
for invalid in (np.zeros((128, 1), dtype=bool), np.zeros(128, dtype=bool),
                np.zeros((128, 128), dtype=np.uint8)):
    try:
        mask_scores(truth, invalid)
    except ValueError:
        pass
    else:
        raise AssertionError("Invalid mask accepted")
checks["shape_and_boolean_contract"] = True
four_connected = np.array([[0,1,0], [1,1,1], [0,1,0]], dtype=bool)
components = {"truth": int(label(truth, structure=four_connected)[1]),
              "broken": int(label(broken, structure=four_connected)[1])}
assert components == {"truth": 1, "broken": 2}
assert results["broken_connection"]["iou"] > 0.98
checks["high_overlap_does_not_preserve_connectivity"] = True

report = {
    "versions": {"python": platform.python_version(), "numpy": np.__version__,
                 "scipy": scipy.__version__, "scikit_learn": sklearn.__version__},
    "data": {"source": "authored binary L-shaped fixture; not a measured PCB image",
             "shape": list(truth.shape), "foreground_pixels": int(truth.sum()),
             "pixel_pitch_mm": None,
             "truth_sha256_uint8_row_major": hashlib.sha256(
                 truth.astype(np.uint8).tobytes()).hexdigest()},
    "results": results,
    "four_connected_component_counts": components,
    "empty_pair_policy": "IoU/Dice null; report separately, do not silently average as a perfect foreground prediction",
    "limits": "No image alignment, threshold selection, neural-network training, electrical test or manufacturing validation performed.",
    "checks": checks,
}
print(json.dumps(report, ensure_ascii=False, indent=2))

۴. خروجی اجرا و پاسخ تمرین‌ها

بازبینی چهار شمارش پیکسلی و پاسخ چهار تمرین
تصویر مفهومی: بازبینی چهار شمارش پیکسلی و پاسخ چهار تمرین؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مثال با Python 3.14.5، NumPy 2.5.4، SciPy 1.18.1 و scikit-learn 1.9.1 اجرا شد و هر هشت بررسی گذشت. برای جابه‌جایی دو ستونی، TP برابر ۱۰۴۰ و هر یک از FP و FN برابر ۱۱۲ است. IoU حدود ۰٫۸۲۳ و Dice حدود ۰٫۹۰۳ می‌شود. این‌ها امتیاز یک تغییر هندسیِ تألیفی‌اند؛ به دقت یک مدل بینایی یا کیفیت یک خط تولید مربوط نیستند. گزارش کامل، همراه شمارش‌ها و هش مرجع، در ادامه آمده است.

{
  "versions": {
    "python": "3.14.5",
    "numpy": "2.5.4",
    "scipy": "1.18.1",
    "scikit_learn": "1.9.1"
  },
  "data": {
    "source": "authored binary L-shaped fixture; not a measured PCB image",
    "shape": [
      128,
      128
    ],
    "foreground_pixels": 1152,
    "pixel_pitch_mm": null,
    "truth_sha256_uint8_row_major": "a9acd6c8e0ceeacfb0b7c0aabb0fc278aaf077e315511a673b65447c3bfd0cc0"
  },
  "results": {
    "perfect": {
      "tp": 1152,
      "fp": 0,
      "fn": 0,
      "tn": 15232,
      "iou": 1.0,
      "dice": 1.0,
      "pixel_accuracy": 1.0,
      "empty_foreground_pair": false
    },
    "shifted_two_columns": {
      "tp": 1040,
      "fp": 112,
      "fn": 112,
      "tn": 15120,
      "iou": 0.8227848101265823,
      "dice": 0.9027777777777778,
      "pixel_accuracy": 0.986328125,
      "empty_foreground_pair": false
    },
    "broken_connection": {
      "tp": 1136,
      "fp": 0,
      "fn": 16,
      "tn": 15232,
      "iou": 0.9861111111111112,
      "dice": 0.993006993006993,
      "pixel_accuracy": 0.9990234375,
      "empty_foreground_pair": false
    },
    "all_background": {
      "tp": 0,
      "fp": 0,
      "fn": 1152,
      "tn": 15232,
      "iou": 0.0,
      "dice": 0.0,
      "pixel_accuracy": 0.9296875,
      "empty_foreground_pair": false
    },
    "empty_both": {
      "tp": 0,
      "fp": 0,
      "fn": 0,
      "tn": 16384,
      "iou": null,
      "dice": null,
      "pixel_accuracy": 1.0,
      "empty_foreground_pair": true
    }
  },
  "four_connected_component_counts": {
    "truth": 1,
    "broken": 2
  },
  "empty_pair_policy": "IoU/Dice null; report separately, do not silently average as a perfect foreground prediction",
  "limits": "No image alignment, threshold selection, neural-network training, electrical test or manufacturing validation performed.",
  "checks": {
    "identity": true,
    "symmetric_overlap_scores": true,
    "count_conservation_and_metric_relation": true,
    "nonempty_iou_matches_sklearn": true,
    "empty_pair_policy_explicit": true,
    "background_accuracy_trap": true,
    "shape_and_boolean_contract": true,
    "high_overlap_does_not_preserve_connectivity": true
  }
}

تمرین اول: IoU و Dice جابه‌جایی دو ستون را از شمارش‌ها حساب کنید. پاسخ: اجتماع برابر ۱۰۴۰+۱۱۲+۱۱۲، یعنی ۱۲۶۴ است؛ IoU برابر ۱۰۴۰ تقسیم بر ۱۲۶۴ می‌شود. مخرج Dice برابر ۲×۱۰۴۰+۱۱۲+۱۱۲، یعنی ۲۳۰۴ است؛ مقدار آن ۲۰۸۰ تقسیم بر ۲۳۰۴ می‌شود. تفاوت دو امتیاز به فرمول مربوط است، نه به دو خروجی تصویری متفاوت.

تمرین دوم: چرا خروجی تماماً پس‌زمینه دقت پیکسلی بالایی دارد؟ پاسخ: ۱۵۲۳۲ پیکسلِ پس‌زمینه درست تشخیص داده شده‌اند و کل تصویر ۱۶۳۸۴ پیکسل دارد. نسبت این دو حدود ۰٫۹۲۹۷ است، اما همهٔ ۱۱۵۲ پیکسل پیش‌زمینه از دست رفته‌اند. گزارشِ فقط دقت پیکسلی، این شکست را پنهان می‌کند؛ معیار پیش‌زمینه و شمارش خطاها باید کنار آن باشند.

تمرین سوم: اگر هر دو ماسک فاقد پیش‌زمینه باشند، آیا باید IoU را حتماً یک گذاشت؟ پاسخ: این انتخاب یک قرارداد است، نه نتیجهٔ تقسیم معمولی. در این برنامه، IoU و Dice مقدار None می‌گیرند و وضعیت خالی علامت می‌خورد. اگر در پروژهٔ دیگری امتیاز یک یا صفر انتخاب شود، باید دلیل، شمار نمونه‌های خالی و اثر آن بر میانگین مشخص باشد؛ مقایسهٔ میانگین‌های با سیاست متفاوت معتبر نیست.

تمرین چهارم: آیا شکاف دو پیکسلی را می‌توان به عرض فیزیکی مشخص تبدیل کرد؟ پاسخ: خیر؛ اندازهٔ فیزیکی پیکسل و تبدیل هندسی معلوم نیست. حتی با داشتن مقیاس، باید جهت اندازه‌گیری و ناحیهٔ تصویر نیز روشن باشد. در این تمرین فقط می‌دانیم ۱۶ پیکسل از یک نوار هشت‌پیکسلی حذف شده است؛ هیچ اندازه‌گیری روی برد واقعی انجام نشده است.

۵. پیوستگی الگو را جداگانه بررسی کنید

تفاوت پیوستگی هندسی الگوی کامل و الگوی دارای شکاف
تصویر مفهومی: تفاوت پیوستگی هندسی الگوی کامل و الگوی دارای شکاف؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در خروجی دارای شکاف، تنها ۱۶ پیکسل حذف شده‌اند. IoU حدود ۰٫۹۸۶ و Dice حدود ۰٫۹۹۳ باقی می‌مانند، اما شمار مؤلفه‌های متصل از یک به دو تغییر می‌کند. برای این بررسی از label در SciPy با همسایگی چهارجهتیِ صریح استفاده کرده‌ایم؛ اتصال قطری در این قرارداد، اتصال محسوب نمی‌شود. معیار اتصال به تعریف همسایه وابسته است و باید همراه نتیجه ثبت شود.

این مشاهده فقط دربارهٔ پیوستگی هندسیِ پیکسل‌های همان ماسک است. اتصال الکتریکی برد چندلایه، کیفیت لحیم یا سلامت قطعه از آن نتیجه نمی‌شود و به شواهد متناسبِ دیگری نیاز دارد. برای تبدیل آزمایش کوچک به فرایند قابل بررسی، نوشتهٔ از نمونهٔ اولیه تا محصول قابل اتکا را ببینید. آستانهٔ قبولی یک معیار باید از نیاز وظیفه بیاید؛ عددی که برای مساحت مناسب است، الزاماً شرط پیوستگی را پوشش نمی‌دهد.

اگر خروجی یک شبکهٔ عصبی را جایگزین ماسکِ تألیفی کنید، همین آزمون‌های معیار همچنان مفیدند، اما کافی نیستند. دادهٔ مستقل، کیفیت مرجع انسانی، تفکیک بردها میان آموزش و آزمون و موارد خارج از توزیع نیز باید بررسی شوند. اینجا نه شبکه‌ای آموزش داده شده و نه عملکرد آن سنجیده شده است. راهنمای انتخاب کاربرد مناسب هوش مصنوعی کمک می‌کند نیاز واقعی را پیش از انتخاب مدل روشن کنید. این آموزش پایه بر مستندات جاری تکیه دارد و خبر تازه معرفی نمی‌شود.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۳ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.