معاينة مختبر آمنة
Open CV
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Open CV
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# الرؤية الحاسوبية وOpenCV
---
هذا الدفتر جزء من منهج «الذكاء الاصطناعي للمبتدئين».
تُعد [OpenCV](https://opencv.org/) المعيار الفعلي لمعالجة الصور. فيها خوارزميات مفيدة كثيرة مطبّقة بلغة C++، ونقدر نستدعيها من Python بعد.
في هالدفتر بنشوف أمثلة على استخدام OpenCV وواجهة OpenCV في Python. وإذا ودكم بتفاصيل أكثر، راجعوا دورة [Learn OpenCV](https://learnopencv.com/getting-started-with-opencv/) على الإنترنت.
أول خطوة نسوي `import cv2`، ومعها نستورد بعض المكتبات المفيدة:
import cv2
import matplotlib.pyplot as plt
import numpy as np
def display_images(images, titles=None, fontsize=12):
images = list(images)[:24]
if not images:
return
columns = min(len(images), 6)
rows = (len(images) + columns - 1) // columns
fig, axes = plt.subplots(rows, columns, squeeze=False, figsize=(columns * 2.5, rows * 2.5))
flat_axes = axes.reshape(-1)
for index, image in enumerate(images):
flat_axes[index].imshow(image)
flat_axes[index].axis("off")
if titles is not None and index < len(titles):
flat_axes[index].set_title(titles[index], fontsize=fontsize)
for axis in flat_axes[len(images):]:
axis.axis("off")
plt.tight_layout()
plt.show()## تحميل الصور
نقدر نمثّل الصور في Python بسهولة بمصفوفات NumPy. مثلًا، صورة رمادية مقاسها 320x200 بكسل تُخزّن في مصفوفة أبعادها 200x320، والصورة الملونة بالمقاس نفسه يكون شكلها 200x320x3، لأن عندنا 3 قنوات لونية.
خلونا نبدأ بتحميل صورة:
# Generated dot pattern replaces the excluded braille photograph.
im = np.full((260, 520, 3), 245, dtype=np.uint8)
for row in range(5):
for column in range(16):
origin_x = 24 + column * 30
origin_y = 28 + row * 46
for dx, dy in ((0, 0), (0, 13), (12, 6), (12, 19)):
if (row + column + dx + dy) % 3:
cv2.circle(im, (origin_x + dx, origin_y + dy), 4, (20, 20, 20), -1)
print(im.shape)
plt.imshow(cv2.cvtColor(im, cv2.COLOR_BGR2RGB))مثل ما تشوفون، هذي صورة لنص مكتوب بطريقة برايل. وبما إن اللون الفعلي مب مهم لنا هنا، نقدر نحوّلها إلى أبيض وأسود:
bw_im = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
print(bw_im.shape)
plt.imshow(bw_im, cmap='gray')## معالجة صورة برايل
إذا بغينا نطبّق **تصنيف الصور**، وهو نوع من **التصنيف**، للتعرّف على النص، لازم نقص كل رمز لحاله عشان يصير قريبًا من صور MNIST اللي شفناها قبل. نقدر نسوي هالشي بتقنية [اكتشاف الأجسام](/learn/ai-for-beginners/object-detection) اللي بنتكلم عنها لاحقًا، أو نجرّب أساليب الرؤية الحاسوبية التقليدية. بتلقون شرحًا زين لفصل الحروف بالرؤية الحاسوبية في [هالتدوينة](https://learnopencv.com/image-alignment-feature-based-using-opencv-c-python/)، وهنا بنركّز على مجموعة من التقنيات الأساسية.
أول شي، خلونا نحسّن الصورة شوي باستخدام **تحديد العتبة**، وهو مشروح بالتفصيل في [مقال OpenCV هذا](https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html):
im = cv2.blur(bw_im,(3,3))
im = cv2.adaptiveThreshold(im, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY_INV, 5, 4)
im = cv2.medianBlur(im, 3)
_,im = cv2.threshold(im, 0, 255, cv2.THRESH_OTSU)
im = cv2.GaussianBlur(im, (3,3), 0)
_,im = cv2.threshold(im, 0, 255, cv2.THRESH_OTSU)
plt.imshow(im)عشان نتعامل مع الصورة، نحتاج «نستخرج» كل نقطة لحالها؛ يعني نحوّلها إلى مجموعة إحداثيات للنقاط. نقدر نسوي هالشي بتقنيات **استخراج السمات** مثل SIFT وSURF و[ORB](https://docs.opencv.org/4.x/d1/d89/tutorial_py_orb.html):
orb = cv2.ORB_create(5000)
f,d = orb.detectAndCompute(im,None)
print(f"First 5 points: { [f[i].pt for i in range(5)]}")خلونا نرسم كل النقاط ونتأكد إن الناتج مضبوط:
def plot_dots(dots):
img = np.zeros((250,500))
for x in dots:
cv2.circle(img,(int(x[0]),int(x[1])),3,(255,0,0))
plt.imshow(img)
pts = [x.pt for x in f]
plot_dots(pts)عشان نفصل الرموز، نحتاج نعرف الصندوق المحيط بالنص كامل. نلقاه ببساطة من أصغر الإحداثيات وأكبرها:
min_x, min_y, max_x, max_y = [int(f([z[i] for z in pts])) for f in (min,max) for i in (0,1)]
min_y+=13
plt.imshow(im[min_y:max_y,min_x:max_x])قد يكون النص مائلًا شوي، وعشان نحاذيه تمامًا نستخدم ما يسمّى **تحويل المنظور**. بناخذ المستطيل المحدد بالنقاط $(x_{min},y_{min}), (x_{min},y_{max}), (x_{max},y_{min}), (x_{max},y_{max})$ ونحاذيه مع صورة جديدة بأبعاد متناسبة:
off = 5
src_pts = np.array([(min_x-off,min_y-off),(min_x-off,max_y+off),
(max_x+off,min_y-off),(max_x+off,max_y+off)])
w = int(max_x-min_x+off*2)
h = int(max_y-min_y+off*2)
dst_pts = np.array([(0,0),(0,h),(w,0),(w,h)])
ho,m = cv2.findHomography(src_pts,dst_pts)
trim = cv2.warpPerspective(im,ho,(w,h))
plt.imshow(trim)بعد ما نحصل على صورة محاذاة زين، يصير تقسيمها إلى أجزاء أسهل بكثير:
char_h = 36
char_w = 24
def slice(img):
dy,dx = img.shape
y = 0
while y+char_h<dy:
x=0
while x+char_w<dx:
# Skip empty lines
if np.max(img[y:y+char_h,x:x+char_w])>0:
yield img[y:y+char_h,x:x+char_w]
x+=char_w
y+=char_h
sliced = list(slice(trim))
display_images(sliced)شفنا إننا نقدر ننجز مهام كثيرة بمعالجة الصور وحدها، من دون ذكاء اصطناعي. وإذا قدرنا نستخدم تقنيات الرؤية الحاسوبية عشان نبسّط شغل الشبكة العصبية، فهذا خيار ممتاز؛ لأنه يساعدنا نحل المشكلة بكمية أقل من بيانات التدريب.
## اكتشاف الحركة بالفرق بين الإطارات
اكتشاف الحركة في بث الفيديو مهمة شائعة جدًا. مثلًا، نقدر نستقبل تنبيه إذا صار شيء قدام كاميرا المراقبة. وإذا بغينا نفهم وش اللي صار، نستخدم الشبكة العصبية؛ لكن تشغيل الشبكة العصبية بس وقت وجود حركة أوفر بكثير.
الفكرة الأساسية بسيطة. إذا كانت الكاميرا ثابتة، المفروض تكون الإطارات متشابهة جدًا. ولأن كل إطار ممثّل بمصفوفة، نطرح مصفوفتَي إطارين متتاليين فنحصل على فرق البكسلات. يكون الفرق قليلًا في الإطارات الساكنة، ويزيد إذا صارت حركة واضحة في الصورة.
بنبدأ بتعلّم فتح ملف فيديو وتحويله إلى سلسلة إطارات:
# Generated motion frames replace the excluded third-party video.
frames = []
for frame_index in range(90):
frame = np.zeros((120, 160, 3), dtype=np.uint8)
if 15 <= frame_index < 75:
start = 10 + (frame_index - 15) * 2
cv2.rectangle(frame, (start, 45), (start + 20, 65), (70, 210, 250), -1)
frames.append(frame)
c = len(frames)
print(f"Total frames: {c}")
display_images(frames[::45])بما إن اللون مب مهم كثير لاكتشاف الحركة، بنحوّل كل الإطارات إلى تدرّج رمادي. بعدها نحسب الفروق بين الإطارات، ونرسم معيار كل فرق عشان نشوف مقدار النشاط بصريًا:
bwframes = [cv2.cvtColor(x,cv2.COLOR_BGR2GRAY) for x in frames]
diffs = [(p2-p1) for p1,p2 in zip(bwframes[:-1],bwframes[1:])]
diff_amps = np.array([np.linalg.norm(x) for x in diffs])
plt.plot(diff_amps)
display_images(diffs[::150],titles=diff_amps[::150])افترضوا إننا نبي تقريرًا يوضح وش صار قدام الكاميرا، ويعرض صورة مناسبة كل مرة يقع فيها حدث. نحتاج نحدد أول إطار وآخر إطار في «الحدث»، ثم نعرض الإطار اللي بالنص. وعشان نقلل التشويش، بننعّم المنحنى اللي فوق بدالة المتوسط المتحرك:
def moving_average(x, w):
return np.convolve(x, np.ones(w), 'valid') / w
threshold = max(1.0, float(diff_amps.max()) * 0.25)
plt.plot(moving_average(diff_amps,10))
plt.axhline(y=threshold, color='r', linestyle='-')الحين نستخدم `np.where` عشان نلقى الإطارات اللي مقدار التغيّر فيها أعلى من العتبة، ثم نستخرج تسلسلًا متصلًا طوله أكثر من 30 إطارًا:
active_frames = np.where(diff_amps > threshold)[0]
def subsequence(seq, min_length=5):
current = []
for frame_index in seq:
frame_index = int(frame_index)
if current and frame_index != current[-1] + 1:
if len(current) > min_length:
return current
current = []
current.append(frame_index)
return current if len(current) > min_length else []
sub = subsequence(active_frames)
if not sub:
raise RuntimeError("The generated motion fixture should contain an active sequence")
print(sub)وأخيرًا نعرض الصورة:
plt.imshow(frames[(sub[0]+sub[-1])//2])يمكن تلاحظون إن ألوان الصورة مب صحيحة. السبب إن OpenCV، لأسباب تاريخية، تحمّل الصور بترتيب BGR، بينما تستخدم matplotlib ترتيب RGB المعتاد. غالبًا الأفضل نحوّل الصور إلى RGB مباشرة بعد تحميلها.
plt.imshow(cv2.cvtColor(frames[(sub[0]+sub[-1])//2],cv2.COLOR_BGR2RGB))## استخراج الحركة بالتدفق البصري
مقارنة إطارين متتاليين تبيّن لنا مقدار التغيّر، لكنها ما تقول لنا وش اللي يتحرك ولا وين رايح. عشان نحصل على هالمعلومة، نستخدم تقنية **[التدفق البصري](https://docs.opencv.org/3.4/d4/dee/tutorial_optical_flow.html)**:
* **التدفق البصري الكثيف** يحسب حقل متجهات يبيّن اتجاه حركة كل بكسل.
* **التدفق البصري المتناثر** يختار سمات مميزة في الصورة، مثل الحواف، ثم يبني مسارها من إطار لإطار.
اقرؤوا أكثر عن التدفق البصري في [هالشرح الممتاز](https://learnopencv.com/optical-flow-in-opencv/).
خلونا نحسب التدفق البصري الكثيف بين الإطارات:
flows = [cv2.calcOpticalFlowFarneback(f1, f2, None, 0.5, 3, 15, 3, 5, 1.2, 0)
for f1,f2 in zip(bwframes[:-1],bwframes[1:])]
flows[0].shapeمثل ما تشوفون، التدفق في كل إطار له أبعاد الإطار نفسها، ومعه قناتان (2) تمثلان مركّبتي x وy لمتجه التدفق البصري.
عرض التدفق البصري ببعدين مب سهل، لكن نقدر نستخدم فكرة ذكية: إذا حوّلناه إلى إحداثيات قطبية، يصير لكل بكسل مكوّنان، *الاتجاه* و*الشدة*. نمثّل الشدة بقيمة البكسل، والاتجاه بألوان مختلفة. بننشئ صورة في [فضاء ألوان HSV (Hue-Saturation-Value)](https://en.wikipedia.org/wiki/HSV_color_space)، بحيث يحدد الاتجاه درجة اللون، وتحدد الشدة القيمة، ويكون التشبع 255.
def flow_to_hsv(flow):
hsvImg = np.zeros((flow.shape[0],flow.shape[1],3),dtype=np.uint8)
mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
hsvImg[..., 0] = 0.5 * ang * 180 / np.pi
hsvImg[..., 1] = 255
hsvImg[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
return cv2.cvtColor(hsvImg, cv2.COLOR_HSV2BGR)
start = sub[0]
stop = sub[-1]
print(start,stop)
frms = [flow_to_hsv(x) for x in flows[start:stop]]
display_images(frms[::25])في هالإطارات، اللون المائل للأخضر يعني حركة لليسار، والأزرق يعني حركة لليمين.
التدفق البصري أداة ممتازة لاستنتاج الاتجاه العام للحركة. مثلًا، إذا شفنا كل بكسلات الإطار تتحرك تقريبًا بالاتجاه نفسه، نقدر نستنتج إن الكاميرا نفسها تحركت ونحاول نعوّض هالحركة.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.