فصل ۹ — آزمایشگاه مستقل چهار عمل مورفولوژی دودویی¶
این Notebook مکمل درس فارسی فصل نهم است. تمام دادهها مصنوعی هستند و برای اجرای آن به اینترنت یا فایل کتاب نیاز نیست. این فایل فقط چهار عمل Erosion، Dilation، Opening و Closing را پیاده میکند؛ رسم نمودار و محاسبه IoU مراحل ارزیابی هستند نه عمل مورفولوژی پنجم.
کتابخانهها: numpy, scipy, matplotlib؛ برای ذخیره فایل PNG بهتر است Pillow نیز نصب باشد.
آزمایش ۱ — تعریف چهار عمل، عنصر ساختاری و منطق همه/حداقل یکی¶
برای دیسک یا مربع مرکزیمتقارن، فرسایش با بررسی تمام نقاط قالب و اتساع با کافیبودن یک تماس ساخته میشود.
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)
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()
آزمایش ۲ — اثر مقیاس و نوع قالب¶
با تغییر شعاع دیسک، برخی پلها و ذرات معتبر نیز حذف میشوند. اثر را روی یک تصویر با دو جسم و پل باریک بررسی کنید.
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 و همکاران، ۲۰۲۶ نشان میدهد مورفولوژی میتواند برای ترکهای باریک مضر باشد؛ اینجا عمداً لکههای نسبتاً پهن را انتخاب میکنیم.
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.
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 را رسم کنید. نمودار به همین مثال مصنوعی وابسته است؛ نباید آن را نتیجه علمی بر داده واقعی معرفی کرد.
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()
آزمایش ۵ — هشدار حذف ساختار باریک¶
یک ترک مصنوعی با ضخامت حدود ۳ پیکسل بسازید؛ گشودن با دیسک شعاع ۳ ممکن است آن را کاملاً حذف کند.
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()))
آزمایش ۶ — چند کنترل خودکار از روابط مجموعهای¶
برای اجسامی دور از مرز قاب، رفتارهای زیر باید برقرار باشند.
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')
تمرین آزاد¶
- جای Opening و Closing را عوض کنید و IoU را مقایسه کنید. ۲. عنصر ساختاری را از
diskبهsquareیاcrossتغییر دهید. ۳. آزمون کنترل مرز را با جسمی که به گوشه چسبیده اجرا کنید و درباره قرارداد بیرون آرایه توضیح دهید. ۴. روی ترک مصنوعی یک عنصر خطی با طول ۵ پیکسل امتحان کنید. مهم: هیچ نتیجهای را بدون ذکر دادهٔ ورودی، قالب و معیار ارزیابی تعمیم ندهید.