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

۱. شدت صحیح، هیستوگرام و قرارداد کلاس‌ها

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

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

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

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

۲. واریانس بین‌کلاسی و تساوی آستانه‌ها

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

برای یک آستانه، شمارِ دو کلاس را n0 و n1 و میانگین شدت‌هایشان را m0 و m1 بگیریم. امتیازِ بین‌کلاسی برابر B = (n0*n1/N²)*(m0-m1)² است؛ N تعداد کل است. می‌خواهیم B را بیشینه کنیم. احتمال‌های تجربیِ دو کلاس از تقسیم شمارها بر N می‌آیند؛ این احتمالِ عیب، اطمینانِ مدل یا احتمالِ درست بودنِ یک پیکسل نیست. واحد B، مربعِ واحدِ سطحِ دیجیتال است.

تابع اصلی برای هر مرز، شمار و مجموعِ شدتِ کلاس پایین را به‌روز می‌کند. اگر S مجموع کل شدت‌ها و S0 مجموعِ پایین باشد، صورتِ قابل مقایسه (N*S0-S*n0)² و مخرجِ آن n0*n1 است. عامل N² برای تمام نامزدها ثابت است. دو نسبت را با ضربِ متقاطعِ اعداد صحیح مقایسه می‌کنیم؛ در انتخاب برنده، تقسیمِ float و تلورانسِ تساوی نداریم. کلاس خالی پیش از هر تقسیم کنار گذاشته می‌شود.

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

۳. کد کاملِ جمع‌های پیشوندی

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

فایل otsu_histogram_contract.py فقط به کتابخانهٔ استاندارد نیاز دارد. با Python 3.14.5 اجرا شده و فرمانِ بازسازی آن python otsu_histogram_contract.py است. scikit-image و SciPy در محیط نصب نیستند و MATLAB هم اجرا نشده است. تابع، آستانه، تمام تساوی‌ها، شمار دو کلاس و امتیازِ کسری را برمی‌گرداند؛ قالبِ خروجی برای بررسیِ تصمیم کافی است و مقدارها را به درصدِ موفقیتِ تصویر تبدیل نمی‌کند.

"""Bounded exact Otsu split; independent within-class Fraction oracle."""
from dataclasses import asdict, dataclass
from fractions import Fraction
from itertools import product
import json
import platform


@dataclass(frozen=True)
class Split:
    threshold: int
    tied_thresholds: tuple
    count: int
    lower_count: int
    upper_count: int
    between_variance: str


def otsu(hist):
    """Digital levels are exactly 0..len(hist)-1; foreground is level > t."""
    if type(hist) is not tuple or not 2 <= len(hist) <= 16:
        raise ValueError("hist must be an exact tuple with 2..16 bins")
    if any(type(n) is not int or not 0 <= n <= 64 for n in hist):
        raise ValueError("each count must be an exact int in 0..64")
    count = sum(hist)
    if not 2 <= count <= 256 or sum(n > 0 for n in hist) < 2:
        raise ValueError("need 2..256 samples and two occupied levels")
    total = sum(level * n for level, n in enumerate(hist))
    lower_n = lower_sum = 0
    best_num, best_den = -1, 1
    ties = []
    for t in range(len(hist) - 1):
        lower_n += hist[t]
        lower_sum += t * hist[t]
        upper_n = count - lower_n
        if not lower_n or not upper_n:
            continue
        numerator = (count * lower_sum - total * lower_n) ** 2
        denominator = lower_n * upper_n
        comparison = numerator * best_den - best_num * denominator
        if comparison > 0:
            best_num, best_den = numerator, denominator
            ties = [t]
        elif comparison == 0:
            ties.append(t)
    threshold = ties[0]
    n0 = sum(hist[:threshold + 1])
    return Split(threshold, tuple(ties), count, n0, count - n0,
                 str(Fraction(best_num, best_den * count * count)))


def within_class_oracle(hist):
    """Expand samples, rebuild both classes, and minimize literal central SSE."""
    samples = [level for level, n in enumerate(hist) for _ in range(n)]
    candidates = []
    for t in range(len(hist) - 1):
        lo = [x for x in samples if x <= t]
        hi = [x for x in samples if x > t]
        if not lo or not hi:
            continue
        m0 = Fraction(sum(lo), len(lo))
        m1 = Fraction(sum(hi), len(hi))
        sse = sum((Fraction(x) - m0) ** 2 for x in lo)
        sse += sum((Fraction(x) - m1) ** 2 for x in hi)
        candidates.append((t, sse))
    best = min(sse for _, sse in candidates)
    ties = tuple(t for t, sse in candidates if sse == best)
    mean = Fraction(sum(samples), len(samples))
    variance = sum((Fraction(x) - mean) ** 2 for x in samples) / len(samples)
    return ties, best, variance - best / len(samples)


def rejected(value):
    try:
        otsu(value)
    except ValueError:
        return True
    return False


def main():
    hist = (2, 0, 1, 0, 0, 1, 0, 2)
    result = otsu(hist)
    oracle_ties, sse, between = within_class_oracle(hist)
    samples = (0, 0, 2, 5, 7, 7)
    checks = {}
    checks['literal_hand_threshold_counts_and_variance'] = (
        result == Split(2, (2, 3, 4), 6, 3, 3, '289/36'))
    checks['independent_hand_within_class_central_sse'] = (
        oracle_ties == (2, 3, 4) and sse == Fraction(16, 3)
        and between == Fraction(289, 36))
    checks['strict_foreground_comparison_changes_boundary_membership'] = (
        tuple(int(x > result.threshold) for x in samples) == (0, 0, 0, 1, 1, 1)
        and sum(x >= result.threshold for x in samples) == 4)
    checked = 0
    for h in product(range(3), repeat=6):
        if sum(n > 0 for n in h) < 2:
            continue
        got = otsu(h)
        ties, _, objective = within_class_oracle(h)
        assert got.tied_thresholds == ties
        assert got.threshold == min(ties)
        assert Fraction(got.between_variance) == objective
        checked += 1
    checks['all_716_small_histograms_independent_oracle_one_group'] = checked == 716
    checks['two_occupied_extreme_levels_choose_first_gap'] = (
        otsu((1, 0, 0, 1)).tied_thresholds == (0, 1, 2))
    shifted = otsu((0, 0) + hist)
    checks['integer_level_translation_preserves_score_and_shifts_ties'] = (
        shifted.threshold == result.threshold + 2
        and shifted.tied_thresholds == tuple(t + 2 for t in result.tied_thresholds)
        and shifted.between_variance == result.between_variance)
    doubled = otsu(tuple(n * 2 for n in hist))
    checks['common_count_scale_preserves_normalized_objective_and_split'] = (
        doubled.threshold == result.threshold
        and doubled.tied_thresholds == result.tied_thresholds
        and doubled.between_variance == result.between_variance
        and doubled.count == result.count * 2)
    asymmetric = otsu((2, 1, 0, 0, 1))
    reflected = otsu((1, 0, 0, 1, 2))
    checks['reflection_maps_entire_tie_set_before_minimum_tie_choice'] = (
        reflected.tied_thresholds == tuple(sorted(3 - t for t in asymmetric.tied_thresholds))
        and reflected.between_variance == asymmetric.between_variance
        and reflected.threshold != 3 - asymmetric.threshold)
    checks['leading_trailing_empty_bins_do_not_create_empty_classes'] = (
        otsu((0, 2, 0, 3, 0)).tied_thresholds == (1, 2))
    checks['inclusive_bin_count_sample_and_per_bin_bounds'] = (
        otsu((1, 1)).threshold == 0
        and otsu((64, 64, 64, 64) + (0,) * 12).count == 256)
    checks['empty_constant_and_total_contract_rejections'] = all(rejected(h) for h in (
        (), (0, 0), (1, 0), (0, 2, 0), (64,) * 5))
    checks['exact_container_and_bin_length_contract_rejections'] = all(rejected(h) for h in (
        [1, 1], iter((1, 1)), '11', (1,), (1,) * 17, None))
    class IntChild(int):
        pass
    checks['exact_integer_count_contract_rejections'] = all(rejected(h) for h in (
        (True, 1), (1.0, 1), (IntChild(1), 1), (-1, 2), (65, 1),
        ('1', 1), (complex(1), 1), (10 ** 1000, 1)))
    assert all(type(v) is bool and v for v in checks.values())
    output = {
        'python': platform.python_version(), 'dependencies': 'stdlib only',
        'skimage_executed': False, 'scipy_executed': False, 'matlab_executed': False,
        'level_unit': 'uncalibrated digital level, spacing one',
        'variance_unit': 'digital-level squared', 'histogram': hist,
        'result': asdict(result), 'hand_within_class_sse': str(sse),
        'hand_total_variance': '107/12', 'strict_foreground_for_hand_samples':
            tuple(int(x > result.threshold) for x in samples),
        'oracle_histograms_inside_one_group': checked,
        'checks': checks, 'unique_check_groups': len(checks), 'all_passed': True,
        'limits': 'Exact bounded histogram computation only; no spatial information, image quality, sensor or segmentation efficacy evidence.'}
    print(json.dumps(output, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()

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

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

۴. مرجع پراکندگی درون‌کلاسی و شاهد دستی

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

آستانه‌های دو، سه و چهار، سه نمونه را در هر کلاس می‌گذارند. میانگینِ پایین دو‌سوم و میانگینِ بالا نوزده‌سوم است؛ فاصلهٔ دو میانگین هفده‌سوم می‌شود. بنابراین B برابر ۲۸۹/۳۶ است. کوچک‌ترین آستانهٔ منتخب دو است و پیش‌زمینه فقط سه مقدارِ پنج، هفت و هفت را می‌گیرد. اگر شرطِ بزرگ‌تر را با بزرگ‌تر یا مساوی عوض کنیم، شدتِ دو نیز اضافه می‌شود و تعداد پیش‌زمینه چهار می‌شود؛ این یک تغییرِ قرارداد است.

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

{
  "python": "3.14.5",
  "dependencies": "stdlib only",
  "skimage_executed": false,
  "scipy_executed": false,
  "matlab_executed": false,
  "level_unit": "uncalibrated digital level, spacing one",
  "variance_unit": "digital-level squared",
  "histogram": [
    2,
    0,
    1,
    0,
    0,
    1,
    0,
    2
  ],
  "result": {
    "threshold": 2,
    "tied_thresholds": [
      2,
      3,
      4
    ],
    "count": 6,
    "lower_count": 3,
    "upper_count": 3,
    "between_variance": "289/36"
  },
  "hand_within_class_sse": "16/3",
  "hand_total_variance": "107/12",
  "strict_foreground_for_hand_samples": [
    0,
    0,
    0,
    1,
    1,
    1
  ],
  "oracle_histograms_inside_one_group": 716,
  "checks": {
    "literal_hand_threshold_counts_and_variance": true,
    "independent_hand_within_class_central_sse": true,
    "strict_foreground_comparison_changes_boundary_membership": true,
    "all_716_small_histograms_independent_oracle_one_group": true,
    "two_occupied_extreme_levels_choose_first_gap": true,
    "integer_level_translation_preserves_score_and_shifts_ties": true,
    "common_count_scale_preserves_normalized_objective_and_split": true,
    "reflection_maps_entire_tie_set_before_minimum_tie_choice": true,
    "leading_trailing_empty_bins_do_not_create_empty_classes": true,
    "inclusive_bin_count_sample_and_per_bin_bounds": true,
    "empty_constant_and_total_contract_rejections": true,
    "exact_container_and_bin_length_contract_rejections": true,
    "exact_integer_count_contract_rejections": true
  },
  "unique_check_groups": 13,
  "all_passed": true,
  "limits": "Exact bounded histogram computation only; no spatial information, image quality, sensor or segmentation efficacy evidence."
}

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

۵. چهار پرسش و مرز کیفیت ماسک

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

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

پرسش دوم: دو برابر شدنِ همهٔ شمارها چه اثری دارد؟ پاسخ: شمار نمونه و اندازهٔ هر کلاس دو برابر می‌شود، ولی میانگین‌ها و سهمِ نسبیِ کلاس‌ها ثابت می‌مانند. در نتیجه آستانه، تساوی‌ها و Bِ نرمال‌شده تغییر نمی‌کنند؛ این ویژگی در کد بررسی شده است.

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

پرسش چهارم: آیا بیشینه شدن B یعنی ماسک درست است؟ پاسخ: خیر؛ جای پیکسل‌ها، سایه، معنای شیء و مرجعِ وظیفه در هیستوگرام نیستند. opening با ردپا و مرز صریح مرحلهٔ هندسیِ دیگری است و آزمون IoU و Dice به ماسکِ مرجع نیاز دارد. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ مستندات 0.26.0 و کدِ منبعِ 0.25.2 جدا ثبت شده‌اند، تاریخ اصلیِ این صفحه‌ها نامعلوم است و این آموزش، خبر تازه یا ادعای تشخیصِ عیب نیست.

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

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