پیدا کردن خط عبور با روش های بینایی کامپیوتر — راهنمای کاربردی

۱۱۶۲ بازدید
آخرین به‌روزرسانی: ۰۴ تیر ۱۴۰۲
زمان مطالعه: ۹ دقیقه
پیدا کردن خط عبور با روش های بینایی کامپیوتر — راهنمای کاربردی

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

پیدا کردن خط عبور با روش های بینایی کامپیوتر

منظور و هدف از پیدا کردن خط عبور با روش های بینایی کامپیوتر (Computer Vision)، دریافت ویدئو از رانندگی در بزرگراه و ترسیم خط عبور خودرو با ساخت یک سلسله مراتب تصویر برای شناسایی و ترسیم بی‌نهایت علامت‌های خط عبور (ترابری) است.

حل مسئله پیدا کردن خط عبور

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

  1. استخراج پیکسل‌های مربوط به سفید و زرد از قاب ویدئویی داده شده
  2. تبدیل تصویر به یک تصویر سیاه و سفید و اعمال جزئی «تاری گاوسی» (Gaussian Blur)
  3. محاسبه مرزها با استفاده از «آشکارساز لبه کنی» (Canny Edge Detector)
  4. اعمال ماسک خاص منطقه‌ای برای آنکه تمرکز صرفا بر جلوی خودرو باشد.
  5. اجرای «تبدیل هاف» (Hough Transform) برای شناسایی خطوط سر راست
  6. تقسیم کردن خطوط به دو دسته خطوط راست و چپ بر اساس شیب آن‌ها
  7. محاسبه نقطه محل تقاطعی خط و نقاط طول از مبدا x
  8. پیگیری آخرین نقاط تقاطع خطوط و نقاط طول از مبدا x در عین هموارسازی از یک فرم به فرم دیگر
  9. ترسیم تقاطع هموار شده و نقاط طول از مبدا x روی قاب

ویدئوی مورد استفاده در این مطلب، در ادامه آمده است.

گام اول: استخراج پیکسل‌های زرد و سفید

برای شروع کار، به نقش این مرحله در انجام این کار و اهمیت آن پرداخته می‌شود.

اگر مستقیم به سراغ «تشخیص لبه‌ها» (Edge Detection) و محاسبه خطوط رفته شود، بدون آنکه ابتدا رنگ‌هایی که برای کاربر اهمیت دارند استخراج شوند، لبه‌های حاصل شده در خروجی ممکن است چیزی شبیه تصویر زیر باشند.

پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۱: تصویر مورد استفاده جهت تشخیص خط عبور
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۲: لبه‌های استخراج شده بدون منزوی کردن رنگ
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۳: لبه‌های استخراج شده با منزوی کردن رنگ

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

ورود به فضای رنگی HSV

انسان‌ها معمولا به رنگ‌ها در فضای «قرمز» (Red)، «سبز» (Green) و «آبی» (‌Blue) یا همان مقادیر RGB می‌اندیشند. در این فضا، نسبت مقادیر سه رنگ بیان شده، رنگی را می‌سازد که یک پیکسل دارد. با RGB، هر پیکسلی رنگ خود را از میزان روشن یا تیره‌تر بودن یکی از سه رنگ بیان شده می‌گیرد، و این امر کاربر را هنگامی که بخواهد رنگی را که متشکل از رنگ‌های روشن متعدد است پیدا کند، با مشکل مواجه می‌سازد.

در فضای رنگی HSV که سرنام سه واژه Hue (رنگ)، «Saturation» (اشباع) و «Value» (مقدار) است، ماجرا به طور کلی متفاوت است. HSV رنگ پیکسل را بر اساس رنگ، اشباع شدگی و مقدار آن مشخص می‌کند. همه پیکسل‌های زرد، صرف‌نظر از اینکه زرد تیره یا روشن هستند، مقدار رنگ مشابهی دارند،. این همان چیزی است که برای اجرای این پروژه به آن نیاز است؛ زیرا در طول مسیر، شرایط نور و جاده بسیار متفاوت است.

برای مطالعه بیشتر پیرامون رنگ‌ها، مطالعه مطلب «اساس نظریه‌ی رنگ‌ها — به زبان ساده» توصیه می‌شود.

استخراج رنگ‌ها

برای درک بهتر چگونگی نمایش انواع رنگ‌های زرد و سفید، پیشنهاد می‌شود که کاربران از این [+] ابزار استفاده کنند. سپس، با ساخت یک ماسک از محلی که رنگ‌های مورد نظر در آن قرار دارند و حذف هر چیز دیگری، این مرحله روی تصویر اعمال می‌شود.

1# Convert image to HSV colorspace
2    hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
3# Define range of colors in HSV
4    lower_yellow = np.array([30,80,80])
5    upper_yellow = np.array([255,255,255])
6    lower_white = np.array([0,0,200])
7    upper_white = np.array([255,20,255])
8# Threshold the HSV image to get only blue colors
9    yellow_mask = cv2.inRange(hsv_image, lower_yellow, upper_yellow)
10    white_mask = cv2.inRange(hsv_image, lower_white, upper_white)
11    mask = cv2.bitwise_or(yellow_mask, white_mask)
12# Bitwise-AND mask the original image
13    color_isolated = cv2.bitwise_and(image, image, mask=mask)

یک نکته مهم که باید به آن توجه داشت این است که باید مقادیر HSV را به جای ۰ تا ۳۶۰ درجه یا ۰ تا ۱۰۰ درصد، از ۰ تا ۲۵۵ بیان کرد.

پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۴: تصویر مورد استفاده برای استخراج پیکسل‌ها زرد و سفید از آن
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۵: خروجی پس از استخراج پیکسل‌های زرد و سفید از تصویر

گام دوم: تبدیل تصویر به سیاه و سفید و هموارسازی

هموارسازی تصویر، روشی برای حذف «نویز» (Noise) از پیکسل‌های خاصی است. این کار کمک می‌کند که انتقال خطوط و لبه‌ها به جای دندانه دندانه بودن، صاف و صیقلی‌تر باشند.

1# Convert to Grayscale
2    gray = cv2.cvtColor(color_isolated,cv2.COLOR_RGB2GRAY)
3# Define a kernel size and apply Gaussian smoothing
4    kernel_size = 5
5    blur_gray = cv2.GaussianBlur(gray,(kernel_size, kernel_size),0)
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۶. تبدیل تصویر رنگی به سیاه و سفید و سپس هموارسازی آن
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۷. تبدیل تصویر با رنگ‌های ایزوله شده به تصویر سیاه و سفید و سپس، هموارسازی آن

گام سوم: شناسایی لبه

کاربر می‌خواهد که همه لبه‌های اطراف ناحیه رنگی را استخراج کند تا خطوط مشخصی را از تصویر استخراج کند. از این کار طی چند گام برای تخمین زدن خط عبورها استفاده می‌شود.

در اینجا، از الگوریتم «آشکارساز لبه کنی» (Canny Edge Detector) با آستانه‌های شناسایی منصفانه‌ای استفاده شده است.

1# Define our parameters for Canny and apply
2    low_threshold = 50
3    high_threshold = 150
4    edges = cv2.Canny(blur_gray, low_threshold, high_threshold)
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۸. نتایج تشخیص لبه
پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۹. بزرگ‌نمایی روی بخشی از تصویر شماره ۸

گام چهارم: ماسک‌گذاری منطقه‌ای

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

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

پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۱۰. محدودسازی جستجوی خط عبور

در ادامه، از متد fillPoly در کتابخانه پردازش تصویر OpenCV برای ساخت یک ماسک منطقه ای و اعمال آن روی تصویر، استفاده شده است.

1# Next we'll create a masked edges image using cv2.fillPoly()
2    mask = np.zeros_like(edges)   
3    ignore_mask_color = 255
4# Define a four sided polygon to mask
5    imshape = image.shape
6    vertices = np.array([
7        [
8            (0,imshape[0]), # bottom left
9            (imshape[1] * .35, imshape[0] * .6),  # top left 
10            (imshape[1] * .65, imshape[0] * .6), # top right
11            (imshape[1],imshape[0]) # bottom right
12        ]
13    ], dtype=np.int32)
14    
15# Do the Masking
16    cv2.fillPoly(mask, vertices, ignore_mask_color)
17    masked_edges = cv2.bitwise_and(edges, mask

اکنون، تصویر به صورت زیر به نظر می‌رسد.

پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۱۱. خروجی حاصل از ماسک‌گذاری منطقه‌ای

گام پنجم: دریافت خطوط با تبدیل هاف

تبدیل هاف یکی از جالب‌ترین الگوریتم‌ها در حوزه بینایی کامپیوتری است. ارائه توضیحات کامل پیرامون این الگوریتم، از حوصله این بخش خارج است؛ ولی به طور کلی می‌توان گفت که این الگوریتم هر پیکسلی را به یک معادله خطی نگاشت می‌کند که در آن، محورهای دو بُعدی که از x و y به m و b می‌روند نشانگر بخش‌های مختلف معادله برای خط y = mx * b‎ هستند و سپس، همه چیز به جای مختصات کارتزین در مختصات قطبی قرار می‌گیرد تا از مشکل تقسیم بر صفر جلوگیری شود.

بخش مهم این مسئله آن است که یک مجموعه از نقاط، به صورت $$x_{1}, y_{1}, x_{2}, y_{2}$$ دریافت می‌شود که نشانگر نقاط آغازین و پایانی خطوطی است که در تصویر پیدا کرده است.

1# Define the Hough transform parameters
2    rho = 2 # distance resolution in pixels
3    theta = np.pi/180 # angular resolution in radians
4    threshold = 30 # minimum number of votes 
5    min_line_length = 20 # minimum number of pixels making up a line
6    max_line_gap = 1 # maximum gap in pixels between lines
7# Run Hough on edge detected image
8    # Output "lines" is an array containing endpoints of lines
9    lines = cv2.HoughLinesP(
10        masked_edges, rho, theta, threshold, 
11        np.array([]), min_line_length, max_line_gap
12    )

گام ششم: شناسایی معادله خط عبور

تبدیل هاف یک مجموعه از نقاط پایانی خط را برای هر خط در اختیار قرار می‌دهد که در تصویر آخر ارائه شده در بالا، می‌توان آن‌ها را مشاهده خوشبختانه، اغلب این خطوط لبه بلند از علامت‌گذاری‌های خطوط عبور را نشان می‌دهند. در آنجا می‌تواند ۵، ۱۰ و یا بیش از ۲۰ خط را بازگردانده شود که این موضوع بستگی به سطح نویز و علامت‌گذاری‌های خط عبور ثبت شده دارد.

گام اول: در این گام، از ریاضیات دوران دبیرستان استفاده شده است و بخش‌های m و b برای هر خط با رابطه کاربردی زیر، پیدا می‌شوند.

1# Iterate over the output "lines" to calculate m's and b's 
2    mxb = np.array([[0,0]])
3    for line in lines:
4        for x1,y1,x2,y2 in line:
5            m = (y2-y1) / (x2-x1)
6            b = y1 + -1 * m * x1   
7            mxb = np.vstack((mxb, [m, b]))

گام دوم: معادله خط، به خطوط عبور چپ و راست تقسیم و شیب میانه و عرض از مبدا y محاسبه می‌شوند. در اینجا، از میانه برای کاهش «دورافتادگی‌ها» (Outliers) استفاده شده است. احتمال وجود دورافتادگی‌ها در صورتی که خطی برای بخش دیگری از جاده یا صحنه که به طور مستقیم به علامت‌گذار خط مرتبط نیستند کشیده شود، وجود دارد. یک موضوع مهم آن است که خط عبور چپ و راست ذاتا دارای جهت‌های مخالفی هستند؛ به گونه‌ای که یکی مثبت و دیگری منفی است. در ادامه، اندیس‌گذاری آرایه (Fancy Indexing) برای محاسبه شیب میانه و رهگیری خطوط راست و چپ شیب، انجام می‌شود.

1median_right_m = np.median(mxb[mxb[:,0] > 0,0])
2median_left_m = np.median(mxb[mxb[:,0] < 0,0])
3median_right_b = np.median(mxb[mxb[:,0] > 0,1])
4median_left_b = np.median(mxb[mxb[:,0] < 0,1])

بسیار عالی؛ اکنون معادله خط برای خطوط حاصل شد.

گام هفتم: محاسبه نقطه تقاطع خط و نقاط طول از مبدا x

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

پیدا کردن خط عبور با روش های بینایی کامپیوتر -- راهنمای کاربردی
شکل ۱۲. فرمول‌های تقاطع خطوط
1# Calculate the Intersect point of our two lines
2    x_intersect = (median_left_b - median_right_b) / (median_right_m - median_left_m)
3    y_intersect = median_right_m * (median_left_b - median_right_b) / (median_right_m - median_left_m) + median_right_b
4# Calculate the X-Intercept Points
5    # x = (y - b) / m
6    left_bottom = (imshape[0] - median_left_b) / median_left_m
7    right_bottom = (imshape[0] - median_right_b) / median_right_m

گام هشتم: ردیابی تاریخچه خط برای هموارسازی آن

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

راهکار آن است که میزان کمی از تعامل‌پذیری برای انتقال هموارسازی شده از معادله خطی که پیش از این دیده شده، قربانی شود. هنگامی که از الگوریتم در تنظیمات ویدئو استفاده می‌شود، الگوریتم به طور فعالانه‌ای ویدئو را به عنوان ورودی دریافت و استفاده می‌کند و تاریخچه‌ای از معادله‌های خط پیشین مشاهده شده را باز می‌گرداند. در ادامه، آنچه بیان شد را می‌توان مشاهده کرد.

1# Create a History array for smoothing
2    num_frames_to_median = 19
3    new_history = [left_bottom, right_bottom, x_intersect, y_intersect]
4    if (history.shape[0] == 1): # First time, create larger array
5        history = new_history
6        for i in range(num_frames_to_median):
7            history = np.vstack((history, new_history))
8    elif (not(np.isnan(new_history).any())): 
9        history[:-1,:] = history[1:]
10        history[-1, :] = new_history
11# Calculate the smoothed line points
12    left_bottom_median = np.median(history[:,0])
13    right_bottom_median = np.median(history[:,1])
14    x_intersect_median = np.median(history[:,2])
15    y_intersect_median = np.median(history[:,3])

این برنامه، حرکت خطوط عبور را به طور نسبتا آهسته‌ای از یک قاب ویدئو را به دیگری معلوم می‌کند و بنابراین، می‌توان شمارش نسبتا سریعی از این داشت که میانه چه تعداد قابی برای محاسبه خط کنونی دریافت می‌شود. پس از مقداری آزمایش، ۱۹ قاب تصویر برای گرفتن میانه انتخاب شد که برای ۲۵ قاب در ثانیه، برابر با تاخیر نظری $$\frac{(\frac{19}{2})}{25}$$ یا ۰/۳۸ ثانیه است. هنگام انجام این کار، صیقل یافتن با تاخیر متوازن می‌شود.

گام نهم: ترسیم خطوط روی قاب‌های ویدئو

گام اول: هنگامی که خطوط نقاط نهایی خط مشخص شدند، می‌توان خطوطی را روی یک تصویر ترسیم کرد.

1# Create a blank image to draw lines on
2line_image = np.copy(image) * 0
3# Create our Lines
4cv2.line(
5    line_image,
6    (np.int_(left_bottom_median), imshape[0]),  
7    (np.int_(x_intersect_median), np.int_(y_intersect_median)),   
8    (255,0,0),10
9)
10cv2.line(
11    line_image,
12    (np.int_(right_bottom_median), imshape[0]),
13    (np.int_(x_intersect_median), np.int_(y_intersect_median)),
14    (0,0,255),10
15)
16# Draw the lines on the image
17    lane_edges = cv2.addWeighted(image, 0.8, line_image, 1, 0)

گام دوم: پردازش کل ویدئو

1history = np.array([[0, 0, 0, 0]]) # Initialize History
2images_list = []
3# Read in Base Video Clip
4base_clip = VideoFileClip(input_filename)
5# Process each video frame, note how "history" is passed around
6for frame in base_clip.iter_frames():
7        [image, history] = draw_lanes_on_img(frame, history)
8        images_list.append(image)
9# Create a new ImageSequenceClip, in other words, our video!
10clip = ImageSequenceClip(images_list, fps=base_clip.fps)
11# Save our video
12%time clip.write_videofile(output_filename, audio=False)

در ادامه، مثال‌هایی از ویدئوهای پردازش شده با کدهای بالا، ارائه شده است.

اگر نوشته بالا برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

بر اساس رای ۳ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
Medium
۱ دیدگاه برای «پیدا کردن خط عبور با روش های بینایی کامپیوتر — راهنمای کاربردی»

با سلام
یه سوال داشتم اینکه ویدئو بدون پردازش رو چه جوری میشه دریافتش کرد و سورس کد اصلی رو دارید منظورم تمام کد به صورت یکجا

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *