چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت چهارم)

مسیریابی خودرو با OpenCV

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

عکس ۱: کروز کنترل تطبیقی
عکس ۲: سیستم کنترل مبتنی بر خطوط

سلام دوستان عزیز،بعد از اینکه بخش سخت افزار (پارت دوم) و نرم افزار (پارت سوم) تکمیل شد حالا در این قسمت فازمون عملی تر و هیجان انگیزتر میشه! عجب چیزیه این OpenCV. اینجاست که مستقیم درگیر مسیریابی خودرومون میشیم. در واقع در این بخش ما مستقیما از OepnCV استفاده میکینم و پکیج های بینایی ماشین که اوپن سورس هستند رو بکار می بریم (که همون OpenCV میگن بهشون) تا بتونیم خودرومون رو بین دو لاین خیابون نگه داریم. در واقع ما در این بخش خودرومون رو خودران می کنیم اما هنوز بهش قابلیت هوش یا یادگیری عمیق نمیدیم ( یادگیری عمیق در بخش ۵ و ۶ بحث میشه).

در حال حاضر تعداد کمی ماشین های ۲۰۱۸-۲۰۱۹ در بازار هستند که هر دو ویژگی کروز کنترل تطبیقی(ACC) و سیستم کنترل مبتنی بر خطوط(LKAS) رو داشته باشند.کروز کنترل تطبیقی از رادار برای برای شناسایی و نگه داشتن یه فاصله امن از ماشین روبرویی استفاده میکنه.این ویژگی ازحدود سال ۲۰۱۲-۲۰۱۳ در دسترسه.سیستم کنترل مبتنی بر خطوط به نسبت ویژگی تازه تری هست و از یک دوربین شیشه جلو(windshield mount) استفاده میکنه تا خطوط مسیر رو شناسایی کنه و بتونه ماشین رو هدایت کنه تا ماشین در وسط خطوط حرکت کنه.این ویژگی موقع رانندگی توی اتوبان خیلی به درد میخوره(هم تو ترافیک و هم مسیر طولانی بدون ترافیک).

مثلا یه خانواده میخواد مسیر شیکاگو تا کلرادو رو زمینی طی کنه که حدود ۳۵ ساعت ممکنه طول بکشه وقتی که با یه ماشینی مثل Volvo XC 90 که هم ACC و هم LKAS داره حدود ۹۵% اون مسیر طولانی خسته کننده رو خود ماشین طی میکنه!!تنها کاری که راننده اون ماشین باید بکنه اینه که فقط دستو بذاره رو فرمون(حتی احتیاجی به حرکت دادنش نیست)و فقط به مسیر نگاه کنه!!اون شخص نه نیاز داره فرمون رو حرکت بده,ترمزی بگیره و یا سرعت ماشین رو کم یا زیاد کنه.تنها ساعاتی که خود ماشین نمیتونه خودش رو برونه وقتیه که برای مثال مسیر با برف پوشونده شده و نمیتونه خطوط رو شناسایی کنه.الان همتون ممکنه با خودتون فکر کنید که این سیستم چه جوری کار میکنه و جالب نمیشه اگه خودمون بتونیم توی ابعاد کوچیکتر اون رو پیاده سازی کنیم؟

تو این بخش میخوایم LKAS رو برای ماشین خودرانمون پیاده سازی کنیم.پیاده سازی ACC به رادار احتیاج داره که ماشین خودران ما اون رو نداره.توی بخش بعدی ممکنه یه سنسور اولتراسونیک رو به ماشین خودران اضافه کنم.اولتراساوند مانند رادار میتواند فاصله ها را تشخیص دهد(البته در بازه نزدیک تر)که برای یه ماشین کوچیکتر روباتیک به دردمون میخوره.

درک:شناسایی خطوط مسیر

یک سیستم کنترل مبتنی بر خطوط دو جز داره به نام های درک(شناسایی خطوط مسیر) و مسیر/برنامه ریزی حرکت(هدایت).شناسایی خطوط وظیفش اینه که یه ویدیو از مسیر رو به مختصات خطوط مسیر شناسایی شده تبدیل کنه.یکی از راه هایی که این کار رو میتونیم انجام بدیم با بینایی کامپیوتر یا همون پکیج open cv هستش(که توی پارت سوم نصبش کردیم).اما قبل از اینکه بتونیم خطوط رو در ویدیو تشخیص بدیم باید بتونیم خطوط در یک عکس تکی تشخیص بدیم.وقتی که این کارو بتونیم انجام بدیم بعدش تشخیص خطوط توی ویدیو مثل تکرار کردن کاری که کردیم برای هر فریم ویدیو میمونه. کار های زیادی باید انجام بدیم پس بیاین شروع کنیم.

ایزوله کردن رنگ مسیر

وقتی که میخوایم برای ماشین خودرانمون مسیر ها رو مشخص کنیم از رنگی استفاده میکنیم که با رنگ بقیه چیزای تو اون محیط متمایز باشه مثلا من از چسب نواری آبی رنگ استفاده کردم چون هم با رنگ بقیه چیزا فرق داره و هم اینکه اثر چسبناکی از خودش باقی نمیذاره.

عکس ۳:چسب برای مشخص کردن
عکس ۴:تصویر اصلی دوربین

عکس ۴ یک ویدیو فریم به دست آمده از دوربین ماشین خودران هستش.اولین کاری که باید انجام بدیم ایزوله کردن نواحی آبی در تصویره برای این کار در ابتدا باید فضای رنگی استفاده شده توسط تصویر که RGB هستش رو به HSV(رنگ-اشباع-مقدار) تبدیل کنیم.ایده اصلی پشتش اینه که تو یه تصویر RGB بخش های مختلف نوار آبی ممکنه به خاطر نور آبی پر رنگ یا آبی کم رنگ باشن در صورتی که توی HSV متغیر رنگکل رنگ نوار رو بدم در نظر گرفتن نور و سایه آبی در نظر میگیره .بهتره که با شکل زیر این رو در نظر بگیریم و توجه داشته باشین که رنگ دو تا نوار تقریبا یکیه.

عکس ۵:تصویر در فضای رنگ HSV

در زیر دستور open cv رو برای انجام این کار مشاهده میکنید:

import cv2
frame = cv2.imread('/home/pi/DeepPiCar/driver/data/road1_240x320.png')
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

دقت کنین که ما BGR رو به HSV تبدیل کردیم نه RGB به HSV به خاطر یه سری قوانین توی OpenCV که عکس ها رو توی فرمت BGR(آبی-سبز-قرمز) میخونه.فضای رنگ جفتشون یکیه و قفقط صرفا ترتیب بیانشون تغییر کرده.

وقتی که تصویرمون توی فرمت HSV هستش میتونیم تمام رنگای طیف آبی رو از تصویر انتخاب کنیم.این کار با استفاده از مشخص کردن یک بازه رنگ آبی هستش.

توی این فضای رنگ,رنگ آبی یه بازه ای حدود درجه های ۱۲۰-۳۰۰ رو شامل میشه(از بین ۰-۳۶۰ درجه).میتونیم به جاش یه بازه کوچیکتر مثل (۱۸۰-۳۰۰)رو برای رنگ آبی در نظر بگیریم اما خیلی مهم نیست.

عکس ۶:Hue(رنگ) در بازه ۰-۳۶۰ درجه

کد زیر برای استخراج رنگ آبی از تصویر توسط OpenCV استفاده میشه:

lower_blue = np.array([60, 40, 40])
upper_blue = np.array([150, 255, 255])
mask = cv2.inRange(hsv, lower_blue, upper_blue)
عکس ۷: ماسک ناحیه آبی

دقت کنین که OpenCV به جای بازه ۰-۳۶۰ از بازه ۰-۱۸۰ استفاده میکنه.بنابراین بازه ای که برای رنگ آبی مشخص میکنیم ۶۰-۱۵۰ هستش(به جای ۱۲۰-۳۰۰).اینا اولین پارامتر های مرز بالا و پایین آرایه ها هستن.پارامتر دوم(اشباع) و سوم(مقدار)خیلی مهم نیستن , میشه گفت که بازه ۴۰-۲۵۵ برای جفتشون به خوبی کار میکنه.

توجه کنین که این ایده دقیقا مثل همون کاریه که استودیو های فیلم سازی و هواشناس هر روز انجام میدن.اونا معمولا از یک صفحه سبز به عنوان backdrop استفاده میکنند تا بتونن اون رنگ سبز رو با نقشه(هواشناسی) و یا دایناسور !(توی فیلم سازی) جایگزین کنن.

شناسایی گوشه های خطوط مسیر

بعدش باید گوشه های تصویر ۷(ماسک فضای آبی) رو شناسایی کنیم تا بتونیم چند تا خط مشخص که نشون دهنده خطوط مسیر آبی هستند رو شناسایی کنیم.

تابع  Canny edge detection function یک دستور قوی هستش که میتونه گوشه های مسیر رو تشخیص بده.تو قطعه کد زیر اولین پارامتر همون ماسک آبی هستش که توی گام قبل به دستش آوردیم.پارامتر دوم و سوم بازه بالا و پایین برای تشخیص گوشه هستن که OpenCV پیشنهاد میده که اونا  (100, 200) و یا (۲۰۰, ۴۰۰) باشن پس ما از (۲۰۰, ۴۰۰) استفاده میکنیم.

edges = cv2.Canny(mask, 200, 400)

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

def detect_edges(frame):
    # filter for blue lane lines
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    show_image("hsv", hsv)
    lower_blue = np.array([60, 40, 40])
    upper_blue = np.array([150, 255, 255])
    mask = cv2.inRange(hsv, lower_blue, upper_blue)
    show_image("blue mask", mask)

    # detect edges
    edges = cv2.Canny(mask, 200, 400)

    return edges

ایزوله کردن ناحیه مورد نظر:

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

پایین یه قطعه کد رو میبینین که اول یه ماسک برای نیمه پایین تصویر درست میکنیم و بعدش اون رو با تصویر گوشه ها ترکیب میکنیم تا به شکل اصلاح شده(سمت راست بالا) برسیم.

def region_of_interest(edges):
    height, width = edges.shape
    mask = np.zeros_like(edges)

    # only focus bottom half of the screen
    polygon = np.array([[
        (۰, height * 1 / 2),
        (width, height * 1 / 2),
        (width, height),
        (۰, height),
    ]], np.int32)

    cv2.fillPoly(mask, polygon, 255)
    cropped_edges = cv2.bitwise_and(edges, mask)
    return cropped_edges

در تصویر اصلاح شده واسه ما آدما بدیهیه که ۴ تا خط پیدا کردیم که نشون دهنده دو تا مسیره در صورتی که برای کامپیوتر اینا فقط یه سری پیکسل سفید رنگ رو یه پس زمینه سیاه رنگن.ما باید مختصات خطوط این مسیر ها رو از پیکسل های سفید دربیاریم.خوش بختانه OpenCV یه تابع خیلی به درد بخور به اسم Hough Transform داره که دقیقا این کار رو انجام میده!Hough Transform یه تکنیک هستش که در پردازش تصویر از بین یه سری پیکسل ویژگی هایی رو مثل خطوط-دایره و بیضی استخراج میکنه. ما از این تکنیک استفاده میکنیم تا خطوط صاف رو از بین یه سری پیکسل سفید شناسایی کنیم.تابع  HoughLinesP سعی میکنه که در پیکسل تا جایی که میتونه خطوط فیت کنه و اونایی که بهشون میخوره که خط تشکیل دادن رو return میکنه .

عکس۱۰: Hough line detection(چپ)-رای گیری(راست)
عکس ۱۱:خطوط شناسایی شده

پایین یه قطعه کد میبینین که برای شناسایی خطوطه.در کل  HoughLinesP خطوط رو با استفاده از مختصات قطبی شناسایی میکنه.مختصات قطبی(زاویه ارتفاع و فاصله از مرکز) به مختصات دکارتی ارجحیت داره چون میتونه هر خطی رو نمایش بده از جمله خطوط عمودی که مختصات دکارتی نمیتونه اونا رو نمایش بده چون شیبشون بینهایته. HoughLinesP پارامتر های زیادی داره از جمله:

  • Rho دقت فاصله بر اساسه پیکسله.ما از یک پیکسل استفاده میکنیم.
  • زاویه دقیت زاویه ای در واحد رادیان هستش(رادین یه روش دیگه برای بیان زاویه هستش مثلا ۱۸۰ درجه براساس رادیان میشه ۳.۱۴۱۵۹ که همون  π هستش).ما از یک درجه رادیان استفاده میکنیم.
  • min_threshold تعداد رای هایی هستش که برای تصویب خط بودن یا نبودن مورد نیازه.اگر یه خطی رای بیشتری داره , Hough Transform در نظر میگیره که این شانس بیشتری برای شناسایی به عنوان خط داره.
  • minLineLength حداقل طول برای خط هستش که در پیکسل بیان میشه.Hough Transform خطی که طولش از این مقدار کمتره رو return نمیکنه.
  • maxLineGap مقدار حداکثری هستش بر اساس پیکسل که دو تا خط میتونن از هم متمایز باشن اما باز هم به عنوان یه خط درنظر گرفته بشن.

مقدار دادن به این پارامتر ها واقعا آزمون و خطاست.تو قطعه کد زیر مقادیری که واسه من به خوبی کار کرده رو آوردم.مشخصه که این مقادیر باید Re-tune بشن تا برای یه حالت تو زندگی واقعی مثل ماشین خودران با دوربینی که دقتش بالاس و مسیری که به صورت خط چین هستش قابل اجرا باشن.

def detect_line_segments(cropped_edges):
    # tuning min_threshold, minLineLength, maxLineGap is a trial and error process by hand
    rho = 1  # distance precision in pixel, i.e. 1 pixel
    angle = np.pi / 180  # angular precision in radian, i.e. 1 degree
    min_threshold = 10  # minimal of votes
    line_segments = cv2.HoughLinesP(cropped_edges, rho, angle, min_threshold, 
                                    np.array([]), minLineLength=8, maxLineGap=4)

    return line_segments

اگه ما خطوط شناسایی شده رو پرینت کنیم میبینیم که نقاط انتهایی(یعنی همون x1,y1) و به دنبالش (x2, y2) که طول هر یک خطوط هستش رو نشون میده.

INFO:root:Creating a HandCodedLaneFollower...
DEBUG:root:detecting lane lines...
DEBUG:root:detected line_segment:
DEBUG:root:[[  7 193 107 120]] of length 123
DEBUG:root:detected line_segment:
DEBUG:root:[[226 131 305 210]] of length 111
DEBUG:root:detected line_segment:
DEBUG:root:[[  1 179 100 120]] of length 115
DEBUG:root:detected line_segment:
DEBUG:root:[[287 194 295 202]] of length 11
DEBUG:root:detected line_segment:
DEBUG:root:[[241 135 311 192]] of length 90
عکس ۱۲:خطوط شناسایی شده توسط Hough Transform

ترکیب قسمت های خطوط برای به دست آوردن دو خط مسیر:

الان که ما کلی قسمت خط با نقاط انتهاییشون (x1,y1),(x2,y2) داریم چه جوری باید اینا رو اینا رو با هم ترکیب کنیم تا فقط به دو تا خط اصلی که بهشون اهمیت میدیم برسیم که اون خط های اصلی چپ و راست ان؟

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

وقتیکه این خطوط رو تقسیم بندی کردیم میانگین شیب ها و عرض از مبدا این قسمت خط ها برای گرفتن شیب ها و عرض از مبدا خطوط چپ و راست رو محاسبه میکنیم.

تابع average_slope_intercept در قطعه کد زیر منطقی که گفتیم رو پیاده سازی میکنه:

def average_slope_intercept(frame, line_segments):
    """
    This function combines line segments into one or two lane lines
    If all line slopes are < 0: then we only have detected left lane
    If all line slopes are > 0: then we only have detected right lane
    """
    lane_lines = []
    if line_segments is None:
        logging.info('No line_segment segments detected')
        return lane_lines

    height, width, _ = frame.shape
    left_fit = []
    right_fit = []

    boundary = 1/3
    left_region_boundary = width * (1 - boundary)  # left lane line segment should be on left 2/3 of the screen
    right_region_boundary = width * boundary # right lane line segment should be on left 2/3 of the screen

    for line_segment in line_segments:
        for x1, y1, x2, y2 in line_segment:
            if x1 == x2:
                logging.info('skipping vertical line segment (slope=inf): %s' % line_segment)
                continue
            fit = np.polyfit((x1, x2), (y1, y2), 1)
            slope = fit[0]
            intercept = fit[1]
            if slope < 0:
                if x1 < left_region_boundary and x2 < left_region_boundary:
                    left_fit.append((slope, intercept))
            else:
                if x1 > right_region_boundary and x2 > right_region_boundary:
                    right_fit.append((slope, intercept))

    left_fit_average = np.average(left_fit, axis=0)
    if len(left_fit) > 0:
        lane_lines.append(make_points(frame, left_fit_average))

    right_fit_average = np.average(right_fit, axis=0)
    if len(right_fit) > 0:
        lane_lines.append(make_points(frame, right_fit_average))

    logging.debug('lane lines: %s' % lane_lines)  # [[[316, 720, 484, 432]], [[1009, 720, 718, 432]]]

    return lane_lines

make_points یه تابع کمکی برای تابع average_slope_intercept هستش که عرض از مبدا و شیب خط رو میگیره و خطوط انتهایی قسمت خط رو برمیگردونه:

def make_points(frame, line):
    height, width, _ = frame.shape
    slope, intercept = line
    y1 = height  # bottom of the frame
    y2 = int(y1 * 1 / 2)  # make points from middle of the frame down

    # bound the coordinates within the frame
    x1 = max(-width, min(2 * width, int((y1 - intercept) / slope)))
    x2 = max(-width, min(2 * width, int((y2 - intercept) / slope)))
    return [[x1, y1, x2, y2]]

به جز منطقی که در بالا گفتیم یه سری موارد خاص هستش که به گفتنشون می ارزه:

۱-یک خط مسیر در تصویر: تو سناریو های عادی ما از دوربین انتظار داریم که هر دو تا خط رو ببینه اما ممکنه بعضی مواقع ماشین از خط خارج بشه شاید به خاطر مشکل منطق هدایت و یا اینکه شاید خطوط خیلی تیز پیچ میخورن. تو این مواقع ممکنه فقط بتونیم یکی از خط ها رو ببینه به خاطر همین کد بالا باید len(right_fit)>0 و len(left_fit)>0 رو چک کنه.

۲-قطعه خطوط عمودی:قطعه خطوط عمودی به ندرت موقع دور زدن ماشین تشخیص داده میشن.با اینکه شناسایی غلطی نیستن چونکه شیبشون بی نهایته نمیتونیم شیبشون رو با بقیه قطعه خطوط میانگین بگیریم.برای راحت تر بودن ازشون چسم پوشی میکنیم.خطوط عمودی خیلی به طور معمول وجود ندارن بنابراین با چشم پوشی ازشون تاثیری روی عملکرد ما گذاشته نمیشه.از سوی دیگه یکی میتونه مختصات X و Y رو جا به جا کنه که در اون صورت شیب ۰ میشه که میتونه توی میانگین محاسبه شه.اما در اون صورت قطعه خط افقی شیب بی نهایت رو خواهد داشت.اما این وضعیت خیلی نادره.راه حل دیگه اینه که قسمت خط ها رو در مختصات قطبی بیان کنیم و بعدش زوایا و فاصله رو میانگین بگیریم.

خلاصه شناسایی مسیر:

با قرار دادن گام های قبلی در کنار هم دیگه در اینحا تابع   detect_lane رو داریم که فریم ویدیو رو به عنوان ورودی میگیره و مختصات دو تا(بعضی مواقع یه دونه)خط رو برمیگردونه

def detect_lane(frame):
    
    edges = detect_edges(frame)
    cropped_edges = region_of_interest(edges)
    line_segments = detect_line_segments(cropped_edges)
    lane_lines = average_slope_intercept(frame, line_segments)
    
    return lane_lines

ما خطوط مسیر رو روی بالای فریم اصلی ویدیو نمایش میدیم:

def display_lines(frame, lines, line_color=(0, 255, 0), line_width=2):
    line_image = np.zeros_like(frame)
    if lines is not None:
        for line in lines:
            for x1, y1, x2, y2 in line:
                cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width)
    line_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1)
    return line_image

lane_lines_image = display_lines(frame, lane_lines)
cv2.imshow("lane lines", lane_lines_image)

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

تصمیم گیری حرکت:هدایت

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

مسیر هایی که دو تا خطشون شناسایی شده:

این حالت آسونیه چون میتونیم جهت مسیر(heading direction) رو با میانگین گیری نقاط انتهایی جفت خطوط مسیر به دست بیاریم.خط قرمز نمایش داده شده توی شکل زیر heading رو نشون میده.یادتون باشه که انتهای پایینی خط قرمز همیشه در وسط پایین شکله.چون ما در نظر میگیریم که دوربین در وسط ماشین نصب شده و مستقیم به روبرو اشاره میکنه.

_, _, left_x2, _ = lane_lines[0][0]
_, _, right_x2, _ = lane_lines[1][0]
mid = int(width / 2)
x_offset = (left_x2 + right_x2) / 2 - mid
y_offset = int(height / 2)
عکس ۱۵:تصویر خطوط شناسایی شده و خط قرمز heading(حرکت)

مسیری که یک خطش شناسایی شده:

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

x1, _, x2, _ = lane_lines[0][0]
x_offset = x2 - x1
y_offset = int(height / 2)

زاویه هدایت(فرمان):

حالا که میدونیم در چه جهتی میخوایم حرکت کنیم باید اونو به زاویه فرمان تبدیل کنیم تا بتونیم به ماشین بگیم که در اون جهت بچرخه.یادتون باشه که واسه این ماشین زاویه ۹۰ درجه مسیر مستقیم,۸۹-۴۵ گردش به چپ و ۱۳۵-۹۱ گردش به راست هستش.در زیر یک رابطه مثلثاتی میبینین که مختصات heading رو به یک زاویه فرمان در واحد درجه تبدیل میکنه.توجه کنین که ماشین خودران برای مردم عادی ایجاد شده بنابراین از گرادیان استفاده نمیکنه.اما توی محاسبات از گرادیان استفاده میشه.

angle_to_mid_radian = math.atan(x_offset / y_offset)  # angle (in radian) to center vertical line
angle_to_mid_deg = int(angle_to_mid_radian * 180.0 / math.pi)  # angle (in degrees) to center vertical line
steering_angle = angle_to_mid_deg + 90  # this is the steering angle needed by picar front wheel

نمایش خط مسیر(heading)

توی عکسای بالا اون heading توی عکس مشخص شده.قطعه کد زیر همین کار رو انجام میده.ورودی همون زاویه هدایت(فرمان) هستش.

def display_heading_line(frame, steering_angle, line_color=(0, 0, 255), line_width=5 ):
    heading_image = np.zeros_like(frame)
    height, width, _ = frame.shape

    # figure out the heading line from steering angle
    # heading line (x1,y1) is always center bottom of the screen
    # (x2, y2) requires a bit of trigonometry

    # Note: the steering angle of:
    # ۰-۸۹ degree: turn left
    # ۹۰ degree: going straight
    # ۹۱-۱۸۰ degree: turn right 
    steering_angle_radian = steering_angle / 180.0 * math.pi
    x1 = int(width / 2)
    y1 = height
    x2 = int(x1 - height / 2 / math.tan(steering_angle_radian))
    y2 = int(height / 2)

    cv2.line(heading_image, (x1, y1), (x2, y2), line_color, line_width)
    heading_image = cv2.addWeighted(frame, 0.8, heading_image, 1, 1)

    return heading_image

پایدارسازی:

اول وقتی که میخوایم زاویه هدایت رو از هر فریم ویدیو به دست بیاریم به ماشین میگیم که در یه زاویه ای حرکت کنه.در صورتی موقع تست توی مسیر واقعی به این نتیجه رسیدیم که ماشین به چپ و راست میپره و بعضی مواقع ممکنه حتی به طور کامل از مسیر خارج شه.بعدش فهمیدیم که علتش به خاطر زاویه فرمان هستش که فریم به فریم محاسبه میشن و خیلی پایدار نیستن.اگه ماشین رو بدون منطق پایدار سازی هدایت بکنید میفهمید که درباره چی دارم میگم.یعضی مواقع زاویه هدایت ممکنه دور و بر ۹۰ درجه باشه برای یه مدتی,اما بعدش به هر علتی زاویه محاسبه شده ممکنه میتونه یهویی تغییر کنه(مثلا بشه ۱۲۰ یا ۷۰).در نتیجه ماشین به سمت راست یا چپ منحرف میشه که مطلوب نیست.پس باید هدایت رو پایدار بکنیم.

توی زندگی واقعی فرمون ماشین وجود داره و میتونیم هر وقت که خواستیم ماشین رو مثلا به سمت راست هدایت کنیم فرمون رو به آرامی حرکت میدیم و زاویه هدایت به عنوان یک دنباله از عداد ۹۰-۹۱-۹۲….۱۳۴-۱۳۵ خواهد بود . نه اینکه تو یه میلی ثانیه از ۹۰ به ۱۳۵ برسیم.

بنابراین استراتژی ما برای پایدار کردن زاویه هدایت به این صورت هستش:اگر زاویه جدید بیشتر از max_angle_deviation درجه از زاویه کنونی باشه,تا max_angle_deviation درجه در جهت زاویه جدید ماشین رو هدایت میکنیم.

def stabilize_steering_angle(
          curr_steering_angle, 
          new_steering_angle, 
          num_of_lane_lines, 
          max_angle_deviation_two_lines=5, 
          max_angle_deviation_one_lane=1):
    """
    Using last steering angle to stabilize the steering angle
    if new angle is too different from current angle, 
    only turn by max_angle_deviation degrees
    """
    if num_of_lane_lines == 2 :
        # if both lane lines detected, then we can deviate more
        max_angle_deviation = max_angle_deviation_two_lines
    else :
        # if only one lane detected, don't deviate too much
        max_angle_deviation = max_angle_deviation_one_lane
    
    angle_deviation = new_steering_angle - curr_steering_angle
    if abs(angle_deviation) > max_angle_deviation:
        stabilized_steering_angle = int(curr_steering_angle
            + max_angle_deviation * angle_deviation / abs(angle_deviation))
    else:
        stabilized_steering_angle = new_steering_angle
    return stabilized_steering_angle

در قطعه کد بالا از دو تا چاشنی max_angle_deviation استفاده کردیم,۵ درجه در صورتی که هر دو تا خط شناسایی شده باشند; ۱ درجه اگه فقط یک خط شناسایی شده باشه.این پارامتر ها برای هر کسی میتونن متفاوت باشن.

در کنار هم قرار دادن:

کد کامل رو توی لینک زیر میتونین پیدا کنین:

https://github.com/dctian/DeepPiCar/tree/master/driver/code

دستور های زیر رو اجرا کنین تا ماشینتون رو روشن کنین:

# skip this line if you have already cloned the repo
pi@raspberrypi:~ $ git clone https://github.com/dctian/DeepPiCar.gitpi@raspberrypi:~ $ cd DeepPiCar/driver/code
pi@raspberrypi:~/DeepPiCar/driver/code $ python3 deep_pi_car.py 
INFO :2019-05-08 01:52:56,073: Creating a DeepPiCar...
DEBUG:2019-05-08 01:52:56,093: Set up camera
DEBUG:2019-05-08 01:52:57,639: Set up back wheels
DEBUG "back_wheels.py": Set debug off
DEBUG "TB6612.py": Set debug off
DEBUG "TB6612.py": Set debug off
DEBUG "PCA9685.py": Set debug off
DEBUG:2019-05-08 01:52:57,646: Set up front wheels
DEBUG "front_wheels.py": Set debug off
DEBUG "front_wheels.py": Set wheel debug off
DEBUG "Servo.py": Set debug off
INFO :2019-05-08 01:52:57,665: Creating a HandCodedLaneFollower..

پایان بخش چهارم!!

پارت های منتشر شده:

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت اول)

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت دوم)

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت سوم)

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت چهارم)

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت پنجم)

چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت ششم)

یک دیدگاه برای “چگونه یک خودرو خودران و هوشمند بسازیم؟ (پارت چهارم)

  1. سلام واقعا ممنون از مطالب خوب تون
    موضوع پروژه من اتومویبل خودران در محیط ویبات هست و از مطالب عالی تون استفاده کردم.

دیدگاهتان را بنویسید

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

قبلا حساب کاربری ایجاد کرده اید؟
گذرواژه خود را فراموش کرده اید؟
Loading...