فصل ۹ — آزمایشگاه مستقل چهار عمل مورفولوژی دودویی¶

این Notebook مکمل درس فارسی فصل نهم است. تمام داده‌ها مصنوعی هستند و برای اجرای آن به اینترنت یا فایل کتاب نیاز نیست. این فایل فقط چهار عمل Erosion، Dilation، Opening و Closing را پیاده می‌کند؛ رسم نمودار و محاسبه IoU مراحل ارزیابی هستند نه عمل مورفولوژی پنجم.

کتابخانه‌ها: numpy, scipy, matplotlib؛ برای ذخیره فایل PNG بهتر است Pillow نیز نصب باشد.

آزمایش ۱ — تعریف چهار عمل، عنصر ساختاری و منطق همه/حداقل یکی¶

برای دیسک یا مربع مرکزی‌متقارن، فرسایش با بررسی تمام نقاط قالب و اتساع با کافی‌بودن یک تماس ساخته می‌شود.

In [ ]:
import numpy as np
from scipy import ndimage as ndi
import matplotlib.pyplot as plt
def footprint(kind: str = 'disk', radius: int = 2):
    """Boolean structuring element centered at [radius, radius]."""
    y, x = np.mgrid[-radius:radius + 1, -radius:radius + 1]
    if kind == 'disk': return x*x + y*y <= radius*radius
    if kind == 'square': return np.ones((2*radius+1,)*2, bool)
    if kind == 'cross': return (x == 0) | (y == 0)
    if kind == 'diamond': return abs(x)+abs(y) <= radius
    if kind == 'horizontal': return y == 0
    raise ValueError(kind)


def erode(binary: np.ndarray, se: np.ndarray) -> np.ndarray:
    """Erosion: all active footprint positions must be foreground."""
    return ndi.binary_erosion(np.asarray(binary, bool), structure=se, border_value=0)


def dilate(binary: np.ndarray, se: np.ndarray) -> np.ndarray:
    """Dilation: foreground expands to footprint positions."""
    return ndi.binary_dilation(np.asarray(binary, bool), structure=se, border_value=0)


def open_mask(binary: np.ndarray, se: np.ndarray) -> np.ndarray:
    return dilate(erode(binary, se), se)


def close_mask(binary: np.ndarray, se: np.ndarray) -> np.ndarray:
    return erode(dilate(binary, se), se)
In [ ]:
A=np.zeros((11,11),bool); A[2:9,2:9]=True; A[5,5]=False
B=footprint('cross',1)
operations=[('Original',A),('Footprint',B),('Erosion',erode(A,B)),('Dilation',dilate(A,B)),('Opening',open_mask(A,B)),('Closing',close_mask(A,B))]
fig,ax=plt.subplots(2,3,figsize=(10,6))
for axes,(title,img) in zip(ax.flat,operations):
    axes.imshow(img,cmap='gray_r',interpolation='nearest',vmin=0,vmax=1)
    axes.set_title(title); axes.set_xticks([]);axes.set_yticks([])
plt.tight_layout();plt.show()

آزمایش ۲ — اثر مقیاس و نوع قالب¶

با تغییر شعاع دیسک، برخی پل‌ها و ذرات معتبر نیز حذف می‌شوند. اثر را روی یک تصویر با دو جسم و پل باریک بررسی کنید.

In [ ]:
test=np.zeros((85,155),bool)
test[15:70,12:63]=True; test[15:70,82:140]=True; test[39:43,63:83]=True
fig,ax=plt.subplots(1,4,figsize=(13,3.4))
for a,r in zip(ax,[1,2,4,7]):
    out=open_mask(test,footprint('disk',r))
    a.imshow(out,cmap='gray_r');a.axis('off');a.set_title(f'opening, r={r}')
plt.tight_layout();plt.show()

آزمایش ۳ — کاربرد معاصر: پاک‌سازی ماسک مصنوعیِ خرابی پهن جاده¶

این سناریو بازسازی آموزشی و نه داده واقعی پهپاد یا مدل آموزش‌دیده است. ایده از کاربرد امروزی بازرسی تصویری جاده الهام گرفته است. مقاله López و همکاران، ۲۰۲۶ نشان می‌دهد مورفولوژی می‌تواند برای ترک‌های باریک مضر باشد؛ اینجا عمداً لکه‌های نسبتاً پهن را انتخاب می‌کنیم.

In [ ]:
def synthetic_road(size=(260, 420), seed=42):
    """Illustration, NOT an actual UAV capture or a trained-model result.

    Returns synthetic road RGB, ground-truth defect mask and noisy predicted mask.
    Defects are broad regions, so a small disk SE is geometrically sensible.
    """
    rng = np.random.default_rng(seed)
    h, w = size
    yy, xx = np.mgrid[:h, :w]
    asphalt = np.clip(0.56 + rng.normal(0, 0.055, size), 0, 1)
    road = np.repeat(asphalt[..., None], 3, axis=2)
    # subtle painted road markings for context; synthetic only
    road[20:23, 50:145, :] = 0.85
    road[20:23, 200:295, :] = 0.85
    road[20:23, 345:415, :] = 0.85
    truth = np.zeros((h,w), bool)
    for cx, cy, rx, ry, angle in [(92, 94, 31, 19, .23), (238, 158, 40, 25, -.17), (345, 84, 25, 20, .21)]:
        px, py = xx-cx, yy-cy
        xr=px*np.cos(angle)+py*np.sin(angle)
        yr=-px*np.sin(angle)+py*np.cos(angle)
        irregular = 0.075*np.sin(np.arctan2(yr, xr)*5+1.5) + 0.055*np.cos(np.arctan2(yr,xr)*7)
        blob=(xr/rx)**2+(yr/ry)**2 <= 1+irregular
        truth |= blob
    road[truth] *= np.array([0.40,0.38,0.36])
    raw=truth.copy()
    # deliberately sparse false positives + internal holes in a thresholded mask
    salt=(rng.random(size)<0.008)
    pepper=(rng.random(size)<0.018)
    raw=(raw | salt) & ~pepper
    # deliberate short false-positive connecting tendril
    raw[153:157, 280:294] = True
    return np.clip(road,0,1), truth, raw


def process_road(raw:np.ndarray, r_open=2, r_close=3, r_edge=2):
    b1=footprint('disk', r_open); b2=footprint('disk',r_close); b3=footprint('disk',r_edge)
    after_erode=erode(raw,b1)
    after_open=dilate(after_erode,b1)
    after_dilate=dilate(after_open,b2)
    after_close=erode(after_dilate,b2)
    inner=erode(after_close,b3)
    outer=dilate(after_close,b3)
    border_band=outer & ~inner
    return dict(raw=raw, eroded=after_erode, opened=after_open, dilated=after_dilate,
                closed=after_close, inner=inner, outer=outer, boundary_band=border_band)


def iou(a,b):
    a=np.asarray(a,bool); b=np.asarray(b,bool)
    u=np.logical_or(a,b).sum()
    return float(np.logical_and(a,b).sum()/u) if u else 1.
In [ ]:
road,truth,raw=synthetic_road()
S=process_road(raw,r_open=2,r_close=3,r_edge=2)
fig,ax=plt.subplots(2,4,figsize=(14,7))
illustrations=[('Synthetic road',road),('Reference mask',truth),('Noisy mask',raw),('Erosion',S['eroded']),('Opening',S['opened']),('Dilation',S['dilated']),('Closing',S['closed']),('Boundary band',S['boundary_band'])]
for a,(title,img) in zip(ax.flat,illustrations):
    a.imshow(img if img.ndim==3 else img.astype(int),cmap=None if img.ndim==3 else 'gray_r',vmin=0 if img.ndim==2 else None,vmax=1 if img.ndim==2 else None)
    a.set_title(title);a.axis('off')
plt.tight_layout();plt.show()
print('Raw IoU:',round(iou(truth,raw),4),'Cleaned IoU:',round(iou(truth,S['closed']),4))

آزمایش ۴ — آیا شعاع بزرگ همیشه بهتر است؟¶

شعاع را تغییر دهید و مقدار IoU را رسم کنید. نمودار به همین مثال مصنوعی وابسته است؛ نباید آن را نتیجه علمی بر داده واقعی معرفی کرد.

In [ ]:
radii=np.arange(1,12)
scores=[]
for r in radii:
    candidate=close_mask(open_mask(raw,footprint('disk',int(r))),footprint('disk',int(r)))
    scores.append(iou(candidate,truth))
plt.plot(radii,scores,'o-',label='Opening then Closing')
plt.axhline(iou(raw,truth),ls='--',color='gray',label='Raw mask')
plt.xlabel('Footprint radius (pixels)');plt.ylabel('IoU');plt.grid(alpha=.3);plt.legend();plt.show()

آزمایش ۵ — هشدار حذف ساختار باریک¶

یک ترک مصنوعی با ضخامت حدود ۳ پیکسل بسازید؛ گشودن با دیسک شعاع ۳ ممکن است آن را کاملاً حذف کند.

In [ ]:
crack=np.zeros((86,180),bool)
for x in range(12,164):
    y=int(40+8*np.sin(x/17));crack[y-1:y+2,x]=True
out=open_mask(crack,footprint('disk',3))
fig,ax=plt.subplots(1,2,figsize=(11,3))
for a,title,ar in zip(ax,['Original thin crack','Opening r=3'],[crack,out]):
    a.imshow(ar,cmap='gray_r');a.set_title(title);a.axis('off')
plt.tight_layout();plt.show()
print('Foreground pixels retained:',int(out.sum()),'of',int(crack.sum()))

آزمایش ۶ — چند کنترل خودکار از روابط مجموعه‌ای¶

برای اجسامی دور از مرز قاب، رفتارهای زیر باید برقرار باشند.

In [ ]:
T=np.zeros((31,31),bool);T[7:24,8:23]=True
S=footprint('disk',2)
assert np.all(erode(T,S)<=T)
assert np.all(T<=dilate(T,S))
assert np.all(open_mask(T,S)<=T)
assert np.all(T<=close_mask(T,S))
assert np.array_equal(open_mask(open_mask(T,S),S),open_mask(T,S))
assert np.array_equal(close_mask(close_mask(T,S),S),close_mask(T,S))
print('Six properties: PASS')

تمرین آزاد¶

  1. جای Opening و Closing را عوض کنید و IoU را مقایسه کنید. ۲. عنصر ساختاری را از disk به square یا cross تغییر دهید. ۳. آزمون کنترل مرز را با جسمی که به گوشه چسبیده اجرا کنید و درباره قرارداد بیرون آرایه توضیح دهید. ۴. روی ترک مصنوعی یک عنصر خطی با طول ۵ پیکسل امتحان کنید. مهم: هیچ نتیجه‌ای را بدون ذکر دادهٔ ورودی، قالب و معیار ارزیابی تعمیم ندهید.