תוכן עניינים:

מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV: 7 שלבים (עם תמונות)
מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV: 7 שלבים (עם תמונות)

וִידֵאוֹ: מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV: 7 שלבים (עם תמונות)

וִידֵאוֹ: מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV: 7 שלבים (עם תמונות)
וִידֵאוֹ: וולוג: מהי בקרת שמירה על נתיב וכיצד היא פועלת? 2024, נוֹבֶמבֶּר
Anonim
מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV
מכונית שמירה על נתיב אוטונומי באמצעות פטל פאי ו- OpenCV

במדריכים אלה יוטמע רובוט שמירה על נתיבים אוטונומיים שיעבור את השלבים הבאים:

  • איסוף חלקים
  • התקנת דרישות תוכנה מוקדמות
  • הרכבת חומרה
  • מבחן ראשון
  • איתור קווי נתיב והצגת הקו המנחה באמצעות openCV
  • יישום בקר PD
  • תוצאות

שלב 1: איסוף רכיבים

איסוף רכיבים
איסוף רכיבים
איסוף רכיבים
איסוף רכיבים
איסוף רכיבים
איסוף רכיבים
איסוף רכיבים
איסוף רכיבים

התמונות למעלה מראות את כל הרכיבים המשמשים בפרויקט זה:

  • מכונית RC: קיבלתי את שלי מחנות מקומית בארצי. הוא מצויד ב -3 מנועים (2 למצערת ואחד להיגוי). החיסרון העיקרי של מכונית זו הוא שההיגוי מוגבל בין "ללא היגוי" ל"היגוי מלא ". במילים אחרות, הוא אינו יכול לנווט בזווית מסוימת, בניגוד למכוניות RC בהיגוי סרוו. תוכלו למצוא מכאן ערכת רכב דומה שתוכננה במיוחד עבור פטל פטל.
  • Raspberry pi 3 דגם b+: זהו מוח המכונית שיטפל בהרבה שלבי עיבוד. הוא מבוסס על מעבד מרובע ליבות של 64 סיביות שעומד על 1.4 GHz. קיבלתי את שלי מכאן.
  • מודול מצלמת Raspberry pi 5 mp: הוא תומך בהקלטות 1080p @ 30 fps, 720p @ 60 fps והקלטות של 640x480p 60/90. הוא תומך גם בממשק סדרתי שניתן לחבר אותו ישירות לפאי הפטל. זו לא האפשרות הטובה ביותר ליישומי עיבוד תמונה, אך היא מספיקה לפרויקט הזה, כמו גם שהוא זול מאוד. קיבלתי את שלי מכאן.
  • נהג מנוע: משמש לבקרת הכיוונים והמהירויות של מנועי DC. הוא תומך בשליטה של 2 מנועי DC בלוח אחד ויכול לעמוד ב 1.5 A.
  • Power Bank (אופציונלי): השתמשתי בבנק כוח (מדורג ב -5V, 3A) כדי להפעיל את פאי הפטל בנפרד. יש להשתמש בממיר הורדה (ממיר באק: זרם פלט 3A) על מנת להפעיל את פאי הפטל ממקור אחד.
  • סוללת 3P (12 V) LiPo: סוללות ליתיום פולימר ידועות בביצועים המעולים שלהן בתחום הרובוטיקה. הוא משמש להפעלת נהג המנוע. קניתי את שלי מכאן.
  • חוטי מגשר בין זכר לזכר ונקבה לנקבה.
  • סרט דו צדדי: משמש להרכבת הרכיבים על מכונית ה- RC.
  • סרט כחול: זהו מרכיב חשוב מאוד בפרויקט זה, הוא משמש ליצירת שני קווי הנתיב שבהם המכונית תיסע. אתה יכול לבחור כל צבע שאתה רוצה אבל אני ממליץ לבחור בצבעים שונים מהסביבה בסביבה.
  • עניבות רוכסן ומוטות עץ.
  • מברג.

שלב 2: התקנת OpenCV ב- Raspberry Pi והגדרת תצוגה מרחוק

התקנת OpenCV ב- Raspberry Pi והגדרת תצוגה מרחוק
התקנת OpenCV ב- Raspberry Pi והגדרת תצוגה מרחוק

שלב זה קצת מעצבן וייקח זמן.

OpenCV (Open Source Computer Vision) היא ספריית תוכנת ראיית מחשב וקוד פתוח ולמידת מכונה. לספרייה יש יותר מ- 2500 אלגוריתמים מותאמים. עקוב אחר המדריך הפשוט הזה להתקנת ה- openCV במערכת הפטל פטל שלך וכן התקנת מערכת ההפעלה פטל פאי (אם עדיין לא עשית זאת). שימו לב שתהליך בניית ה- openCV עשוי להימשך כ -1.5 שעות בחדר מקורר היטב (מכיוון שטמפרטורת המעבד תעלה מאוד!) אז שתו תה ותחכו בסבלנות: D.

לתצוגה המרוחקת, עקוב אחר המדריך הזה להגדרת גישה מרחוק לפאי הפטל שלך ממכשיר Windows/Mac שלך.

שלב 3: חיבור חלקים יחד

חיבור חלקים ביחד
חיבור חלקים ביחד
חיבור חלקים ביחד
חיבור חלקים ביחד
חיבור חלקים ביחד
חיבור חלקים ביחד

התמונות למעלה מראות את הקשרים בין פטל פאי, מודול מצלמה ונהג מנוע. שימו לב כי המנועים בהם השתמשתי סופגים 0.35 A ב -9 V כל אחד מה שמקל על נהג המנוע להפעיל 3 מנועים במקביל. ומכיוון שאני רוצה לשלוט במהירות 2 המנועים המצערים (1 אחורי ו -1 קדמי) בדיוק באותו אופן, חיברתי אותם לאותה יציאה. הרכבתי את נהג המנוע בצד ימין של המכונית באמצעות סרט כפול. באשר למודול המצלמה, הכנסתי קשר בין רכסי הבורג כפי שמופיע בתמונה למעלה. לאחר מכן, אני מחבר את המצלמה לסרגל עץ כך שאוכל להתאים את מיקום המצלמה כרצוני. נסה להתקין את המצלמה באמצע המכונית ככל האפשר. אני ממליץ למקם את המצלמה לפחות 20 ס מ מעל הקרקע כך ששדה הראייה מול המכונית ישתפר. הסכימה של פריטינג מצורפת למטה.

שלב 4: מבחן ראשון

מבחן ראשון
מבחן ראשון
מבחן ראשון
מבחן ראשון

בדיקת מצלמות:

לאחר התקנת המצלמה וספריית openCV בנויה, הגיע הזמן לבדוק את התמונה הראשונה שלנו! נצלם תמונה מ- pi cam ונשמור אותה כ- "original.jpg". ניתן לעשות זאת בשתי דרכים:

1. שימוש בפקודות מסוף:

פתח חלון מסוף חדש והקלד את הפקודה הבאה:

raspistill -o original.jpg

זה ייקח תמונת סטילס ותשמור אותה בספריית "/pi/original.jpg".

2. שימוש בכל IDE של פייתון (אני משתמש ב- IDLE):

פתח סקיצה חדשה וכתוב את הקוד הבא:

יבוא cv2

video = cv2. VideoCapture (0) בעוד True: ret, frame = video.read () frame = cv2.flip (frame, -1) # המשמש להפוך את התמונה אנכית cv2.imshow ('מקורי', מסגרת) cv2. imwrite ('original.jpg', frame) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

בואו נראה מה קרה בקוד הזה. השורה הראשונה היא ייבוא ספריית openCV שלנו כדי להשתמש בכל הפונקציות שלה. הפונקציה VideoCapture (0) מתחילה להזרים וידאו חי מהמקור שנקבע על ידי פונקציה זו, במקרה זה היא 0 שמשמעותה מצלמת raspi. אם יש לך מצלמות מרובות, יש להציב מספרים שונים. video.read () יקרא כל מסגרת מגיעה מהמצלמה ותשמור אותה במשתנה שנקרא "מסגרת". הפונקציה flip () תהפוך את התמונה ביחס לציר y (אנכית) מכיוון שאני מתקינה את המצלמה הפוכה. imshow () יציג את המסגרות שלנו בראשות המילה "מקורי" ו- imwrite () ישמור את התמונה שלנו כ- original.jpg. waitKey (1) ימתין למשך 1 ms עד לחיצה על לחצן כל מקלדת ותחזיר את קוד ASCII שלו. אם לוחצים על כפתור הבריחה (esc), ערך עשרוני של 27 יוחזר ויפרוץ את הלולאה בהתאם. video.release () יפסיק להקליט ולהרוס AllWindows () יסגור כל תמונה שנפתחה על ידי הפונקציה imshow ().

אני ממליץ לבדוק את התמונה שלך בשיטה השנייה כדי להכיר את פונקציות openCV. התמונה נשמרת בספריית "/pi/original.jpg". התמונה המקורית שהצלמה שלי צילמה מוצגת למעלה.

בדיקת מנועים:

שלב זה חיוני לקביעת כיוון הסיבוב של כל מנוע. ראשית, בואו נקבל מבוא קצר על עקרון העבודה של נהג מנוע. התמונה למעלה מציגה את סיכת החוצה של נהג המנוע. אפשר A, קלט 1 וכניסה 2 קשורים לבקרת מנוע A. אפשר B, קלט 3 וכניסה 4 קשורים לבקרת מנוע B. בקרת כיוון נקבעת על ידי חלק "קלט" ובקרת המהירות נקבעת על ידי חלק "אפשר". כדי לשלוט בכיוון המנוע A למשל, הגדר את קלט 1 ל- HIGH (3.3 V במקרה זה מכיוון שאנו משתמשים בפאי פטל) והגדר את קלט 2 ל- LOW, המנוע יסתובב בכיוון ספציפי ועל ידי הגדרת הערכים ההפוכים. לכניסה 1 ולכניסה 2, המנוע יסתובב בכיוון ההפוך. אם קלט 1 = קלט 2 = (גבוה או נמוך), המנוע לא יסתובב. הפעל סיכות לקחת אות קלט אפנון רוחב (PWM) מהפטל (0 עד 3.3 V) והפעל את המנועים בהתאם. לדוגמה, אות 100% PWM פירושו שאנחנו עובדים על המהירות המרבית ואות 0% PWM פירושו שהמנוע אינו מסתובב. הקוד הבא משמש לקביעת כיווני המנועים ובדיקת מהירותם.

זמן יבוא

ייבוא RPi. GPIO כ- GPIO GPIO.setwarnings (שקר) # סיכות מנוע היגוי Steering_enable = 22 # פין פיזי 15 in1 = 17 # פין פיזי 11 in2 = 27 # פין פיזי 13 # מנועי חנק סיכות מצערת_אפשר = 25 # פין פיזי 22 in3 = 23 # פין פיזי 16 in4 = 24 # פין פיזי 18 GPIO.setmode (GPIO. BCM) # השתמש במספור GPIO במקום במספור פיזי GPIO.setup (in1, GPIO.out) GPIO.setup (in2, GPIO.out) GPIO. setup (in3, GPIO.out) GPIO.setup (in4, GPIO.out) GPIO.setup (throttle_enable, GPIO.out) GPIO.setup (steering_enable, GPIO.out) # בקרת מנוע היגוי GPIO.output (in1, GPIO. HIGH) GPIO.output (in2, GPIO. LOW) היגוי = GPIO. PWM (Steering_enable, 1000) # הגדר את תדר המיתוג ל 1000 Hz Steering.stop () # בקרת מנועי מצערת GPIO.output (in3, GPIO. HIGH) GPIO.output (in4, GPIO. LOW) מצערת = GPIO. PWM (throttle_enable, 1000) # הגדר את תדר המיתוג ל- 1000 Hz throttle.stop () time.sleep (1) throttle.start (25) # מניע את המנוע ב -25 אות PWM % -> (0.25 * מתח סוללה) - של נהג אובדן היגוי.התחלה (100) # מניע את המנוע באות 100% PWM-> (1 * מתח סוללה) - זמן אובדן הנהג. שינה (3) מצערת. עצירה () היגוי עצירה ()

קוד זה יפעיל את מנועי החנק ומנוע ההיגוי למשך 3 שניות ולאחר מכן יעצור אותם. ניתן לקבוע את (אובדן הנהג) באמצעות מד מתח. לדוגמה, אנו יודעים שאות 100 PWM אמור לתת את המתח המלא של הסוללה במסוף המנוע. אבל, על ידי הגדרת PWM ל- 100%, גיליתי שהנהג גורם לירידה של 3 V והמנוע מקבל 9 V במקום 12 V (בדיוק מה שאני צריך!). ההפסד אינו לינארי כלומר ההפסד ב -100% שונה מאוד מההפסד ב -25%. לאחר הפעלת הקוד לעיל, התוצאות שלי היו כדלקמן:

תוצאות חנק: אם in3 = HIGH ו- in4 = LOW, מנועי המצערת יהיו בעלי סיבוב Clock-Wise (CW) כלומר המכונית תנוע קדימה. אחרת המכונית תנוע לאחור.

תוצאות היגוי: אם in1 = HIGH ו- in2 = LOW, מנוע ההיגוי יסתובב בשמאלו המרבי כלומר המכונית תנווט שמאלה. אחרת המכונית תנווט ימינה. לאחר כמה ניסויים, גיליתי שמנוע ההיגוי לא יסתובב אם אות ה- PWM לא היה 100% (כלומר המנוע ינווט במלואו ימינה או לגמרי שמאלה).

שלב 5: איתור קווי נתיב וחישוב קו הכותרת

איתור קווי נתיב וחישוב קו הכותרת
איתור קווי נתיב וחישוב קו הכותרת
איתור קווי נתיב וחישוב קו הכותרת
איתור קווי נתיב וחישוב קו הכותרת
איתור קווי נתיב וחישוב קו הכותרת
איתור קווי נתיב וחישוב קו הכותרת

בשלב זה יוסבר האלגוריתם שישלוט בתנועת המכונית. התמונה הראשונה מציגה את כל התהליך. קלט המערכת הוא תמונות, הפלט הוא תטא (זווית היגוי במעלות). שים לב כי העיבוד מתבצע על תמונה אחת ויחזור על כל המסגרות.

מַצלֵמָה:

המצלמה תתחיל להקליט וידאו ברזולוציה (320 x 240). אני ממליץ להוריד את הרזולוציה כדי שתוכל לקבל קצב פריימים טוב יותר (fps) מכיוון שירידת fps תתרחש לאחר החלת טכניקות עיבוד על כל פריים. הקוד שלהלן יהיה הלולאה הראשית של התוכנית ויוסיף כל שלב על קוד זה.

יבוא cv2

ייבוא numpy כ- np video = cv2. VideoCapture (0) video.set (cv2. CAP_PROP_FRAME_WIDTH, 320) # הגדר את הרוחב ל- 320 p video.set (cv2. CAP_PROP_FRAME_HEIGHT, 240) # הגדר את הגובה ל 240 p # הלולאה בעוד נכון: ret, frame = video.read () frame = cv2.flip (frame, -1) cv2.imshow ("original", frame) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

הקוד כאן יציג את התמונה המקורית שהתקבלה בשלב 4 ומוצג בתמונות למעלה.

המרה למרחב צבע HSV:

כעת לאחר צילום הקלטת וידיאו כמסגרות מהמצלמה, השלב הבא הוא המרת כל מסגרת למרחב צבע גוון, רוויה וערך (HSV). היתרון העיקרי בכך הוא היכולת להבחין בין הצבעים לפי רמת הזוהר שלהם. והנה הסבר טוב על מרחב הצבעים של HSV. ההמרה ל- HSV מתבצעת באמצעות הפונקציה הבאה:

def convert_to_HSV (מסגרת):

hsv = cv2.cvtColor (frame, cv2. COLOR_BGR2HSV) cv2.imshow ("HSV", hsv) החזר hsv

פונקציה זו תקרא מהלולאה הראשית ותחזיר את המסגרת בחלל צבע HSV. המסגרת שהושגה על ידי בחלל צבע HSV מוצגת למעלה.

זיהוי צבע כחול וקצוות:

לאחר המרת התמונה למרחב צבע HSV, הגיע הזמן לזהות רק את הצבע שמעניין אותנו (כלומר הצבע הכחול מכיוון שהוא הצבע של קווי הנתיב). כדי לחלץ צבע כחול ממסגרת HSV, יש לציין טווח של גוון, רוויה וערך. עיין כאן כדי לקבל מושג טוב יותר לגבי ערכי HSV. לאחר כמה ניסויים, הגבולות העליונים והתחתונים של הצבע הכחול מוצגים בקוד שלהלן. וכדי לצמצם את העיוות הכולל בכל מסגרת, קצוות מזוהים רק באמצעות גלאי קצה ערמומי. כאן אפשר למצוא מידע נוסף על קאני אדג '. כלל אצבע הוא לבחור את הפרמטרים של פונקציית Canny () ביחס של 1: 2 או 1: 3.

def detect_edges (frame):

מתחת_כחול = np.array ([90, 120, 0], dtype = "uint8") # גבול תחתון של צבע כחול upper_blue = np.array ([150, 255, 255], dtype = "uint8") # גבול עליון של מסכת צבע כחול = cv2.inRange (hsv, lower_blue, upper_blue) # מסכה זו תסנן הכל חוץ מכחול # זיהוי קצוות קצוות = cv2. Canny (מסכה, 50, 100) cv2.imshow ("קצוות", קצוות) קצוות החזרה

פונקציה זו תקרא גם מהלולאה הראשית שלוקחת כפרמטר את מסגרת מרחב הצבעים של HSV ומחזירה את המסגרת הקצוות. המסגרת הקצוות שאכן קיבלתי נמצאת למעלה.

בחר אזור עניין (ROI):

בחירת אזור העניין היא קריטית להתמקד רק באזור אחד של המסגרת. במקרה זה, אני לא רוצה שהמכונית תראה הרבה פריטים בסביבה. אני רק רוצה שהמכונית תתמקד בקווי הנתיב ותתעלם מכל דבר אחר. P. S: מערכת הקואורדינטות (צירים x ו- y) מתחילה מהפינה השמאלית העליונה. במילים אחרות, הנקודה (0, 0) מתחילה מהפינה השמאלית העליונה. ציר y הוא הגובה וציר x הוא הרוחב. הקוד להלן בוחר אזור עניין להתמקד רק בחצי התחתון של המסגרת.

def region_of_interest (קצוות):

גובה, רוחב = קצוות.צורה # חלץ את הגובה והרוחב של מסכת מסגרת הקצוות = np.zeros_like (קצוות) # צור מטריצה ריקה עם אותן מידות של מסגרת הקצוות # התמקד רק בחצי התחתון של המסך # ציין את הקואורדינטות של 4 נקודות (שמאל למטה, עליון שמאל, עליון מימין, ימין למטה) מצולע = np.array (

פונקציה זו תיקח את המסגרת הקצועה כפרמטר ותצייר מצולע עם 4 נקודות מוגדרות מראש. הוא יתמקד רק במה שבתוך המצולע ויתעלם מכל מה שמחוצה לו. מסגרת אזור העניין שלי מוצגת למעלה.

איתור פלחי קו:

Hough transform משמש לאיתור מקטעי קו ממסגרת קצוות. Hough Transform היא טכניקה לאיתור כל צורה בצורה מתמטית. הוא יכול לזהות כמעט כל אובייקט גם אם הוא מעוות על פי מספר קולות כלשהו. מוצגת כאן הפניה מצוינת לשינוי Hough. עבור יישום זה, הפונקציה cv2. HoughLinesP () משמשת לזיהוי קווים בכל מסגרת. הפרמטרים החשובים של פונקציה זו הם:

cv2. HoughLinesP (frame, rho, theta, min_threshold, minLineLength, maxLineGap)

  • מסגרת: היא המסגרת בה אנו רוצים לזהות קווים.
  • rho: זהו דיוק המרחק בפיקסלים (בדרך כלל הוא = 1)
  • תטא: דיוק זוויתי ברדיאנים (תמיד = np.pi/180 ~ 1 מעלה)
  • min_threshold: מינימום הצבעה שהיא אמורה לקבל כדי שייחשב כשורה
  • minLineLength: אורך מינימלי של קו בפיקסלים. כל שורה קצרה ממספר זה אינה נחשבת כקו.
  • maxLineGap: פער מרבי בפיקסלים בין 2 שורות שיש להתייחס אליהן כשורה אחת. (הוא אינו משמש במקרה שלי מכיוון שלקווי הנתיב בהם אני משתמש אין פער).

פונקציה זו מחזירה את נקודות הקצה של שורה. הפונקציה הבאה נקראת מהלולאה הראשית שלי לאיתור קווים באמצעות טרנספורמציה של Hough:

def detect_line_segments (חתוכים_קצוות):

rho = 1 theta = np.pi / 180 min_threshold = 10 line_segments = cv2. HoughLinesP (חתכים_קצוות, rho, theta, min_threshold, np.array (), minLineLength = 5, maxLineGap = 0) line_segments

שיפוע ממוצע ויירוט (m, b):

זכור כי משוואת הקו ניתנת על ידי y = mx + b. כאשר m הוא שיפוע הקו ו- b הוא יירוט יי. בחלק זה יחושב ממוצע השיפועים והיירוט של מקטעי הקווים שזוהו באמצעות טרנספורמציה של Hough. לפני שתעשה זאת, הבה נסתכל על תמונת המסגרת המקורית המוצגת למעלה. נראה שהנתיב השמאלי עולה כלפי מעלה ולכן יש לו שיפוע שלילי (זוכרים את נקודת ההתחלה של מערכת הקואורדינטות?). במילים אחרות, לקו הנתיב השמאלי יש x1 <x2 ו- y2 x1 ו- y2> y1 אשר ייתן שיפוע חיובי. לכן, כל הקווים בעלי שיפוע חיובי נחשבים לנקודות נתיב ימניות. במקרה של קווים אנכיים (x1 = x2), השיפוע יהיה אינסופי. במקרה זה, נדלג על כל הקווים האנכיים כדי למנוע קבלת שגיאה. כדי להוסיף דיוק רב יותר לגילוי זה, כל מסגרת מחולקת לשני אזורים (ימין ושמאל) באמצעות 2 קווי גבול. כל נקודות הרוחב (נקודות ציר x) גדולות יותר מקו הגבול הימני, קשורות לחישוב הנתיב הימני. ואם כל נקודות הרוחב פחותות מקו הגבול השמאלי, הן משויכות לחישוב נתיב שמאל. הפונקציה הבאה לוקחת את המסגרת תחת עיבוד וקטעי נתיבים שזוהו באמצעות טרנספורמציה Hough ומחזירה את השיפוע הממוצע והיירוט של שני קווי נתיב.

def average_slope_intercept (frame, line_segments):

lane_lines = אם line_segments הוא ללא: הדפסה ("לא זוהה קטע קו") החזר lane_lines גובה, רוחב, _ = frame.shape left_fit = right_fit = boundary = left_region_boundary = רוחב * (1 - גבול) right_region_boundary = רוחב * גבול עבור line_segment ב- line_segments: עבור x1, y1, x2, y2 ב- line_segment: אם x1 == x2: הדפסה ("דילוג על קווים אנכיים (שיפוע = אינסוף)") המשך התאמה = np.polyfit ((x1, x2), (y1, y2), 1) שיפוע = (y2 - y1) / (x2 - x1) יירוט = y1 - (שיפוע * x1) אם שיפוע <0: אם x1 <שמאל_אזור_גבול ו x2 ימני_גבול ו x2> ימני_גבול: ימין_תאמת. לצרף ((שיפוע, ליירט)) 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) אם len (right_fit)> 0: lane_lines.append (make_points (frame, right_fit_average)) # lane_lines הוא מערך דו-ממדי המורכב מהקואורדינטות של קווי הנתיב הימני והשמאלי # למשל: lan e_lines =

make_points () היא פונקציית עוזר לפונקציה average_slope_intercept () שתחזיר את הקואורדינטות המוגבלות של קווי הנתיב (מלמטה לאמצע המסגרת).

def make_points (מסגרת, שורה):

גובה, רוחב, _ = שיפוע מסגרת.צורה, יירוט = קו y1 = גובה # תחתית המסגרת y2 = int (y1 / 2) # צרו נקודות מאמצע המסגרת למטה אם שיפוע == 0: שיפוע = 0.1 x1 = int ((y1 - יירוט) / שיפוע) x2 = int ((y2 - יירוט) / שיפוע) החזרה

כדי למנוע חלוקה ב- 0, מוצג תנאי. אם שיפוע = 0 שפירושו y1 = y2 (קו אופקי), תן למדרון ערך קרוב ל- 0. זה לא ישפיע על הביצועים של האלגוריתם כמו גם שזה ימנע מקרה בלתי אפשרי (חלוקה ב- 0).

כדי להציג את קווי הנתיב במסגרות, הפונקציה הבאה משמשת:

def display_lines (frame, lines, line_color = (0, 255, 0), line_width = 6): # קו צבע (B, G, R)

line_image = np.zeros_like (מסגרת) אם השורות אינן None: עבור שורה בשורות: עבור x1, y1, x2, y2 בשורה: 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

הפונקציה cv2.addWeighted () לוקחת את הפרמטרים הבאים והיא משמשת לשילוב שתי תמונות אך עם מתן משקל לכל אחת מהן.

cv2.addWeighted (image1, alpha, image2, beta, gamma)

ומחשב את תמונת הפלט באמצעות המשוואה הבאה:

פלט = אלפא * תמונה 1 + בטא * תמונה 2 + גמא

מידע נוסף אודות הפונקציה cv2.addWeighted () נגזר כאן.

חישוב והצגת קו כותרת:

זהו השלב האחרון לפני שאנו מפעילים מהירות על המנועים שלנו. קו הכותרת אחראי לתת למנוע ההיגוי את הכיוון אליו עליו להסתובב ולתת למנועי החנק את המהירות שבה הם יפעלו. חישוב קו הכותרת הוא טריגונומטריה טהורה, נעשה שימוש בפונקציות טריגונומטריות שיזוף ואטאן (tan^-1). מקרים קיצוניים מסוימים הם כאשר המצלמה מזהה קו נתיב אחד בלבד או כאשר היא אינה מזהה קו כלשהו. כל המקרים הללו מוצגים בפונקציה הבאה:

def get_steering_angle (frame, lane_lines):

גובה, רוחב, _ = frame.shape אם len (lane_lines) == 2: # אם מזוהים שני קווי נתיב _, _, left_x2, _ = lane_lines [0] [0] # חלץ שנותר x2 ממערך lane_lines _, _, right_x2, _ = lane_lines [1] [0] # חלץ ימני x2 ממערך lane_lines mid = int (רוחב / 2) x_offset = (left_x2 + right_x2) / 2 - אמצע y_offset = int (גובה / 2) elif len (lane_lines)) == 1: # אם רק שורה אחת מזוהה x1, _, x2, _ = קו נתיבים [0] [0] x_offset = x2 - x1 y_offset = int (גובה / 2) elif len (lane_lines) == 0: # אם לא מזוהה קו x_offset = 0 y_offset = int (גובה / 2) angle_to_mid_radian = math.atan (x_offset / y_offset) angle_to_mid_deg = int (angle_to_mid_radian * 180.0 / math.pi) steering_angle = angle_to_mid_deg + 90 חזרה היגוי

x_offset במקרה הראשון הוא כמה הממוצע ((ימין x2 + שמאל x2) / 2) שונה מאמצע המסך. y_offset תמיד נחשב לגובה / 2. התמונה האחרונה למעלה מציגה דוגמה לשורת כותרת. angle_to_mid_radians זהה ל"תטא "המוצג בתמונה האחרונה למעלה. אם rat_angle = 90, המשמעות היא שלמכונית יש קו כיוון הניצב לקו "גובה / 2" והמכונית תנוע קדימה ללא היגוי. אם היגוי_סבך> 90, המכונית צריכה לכוון ימינה אחרת היא צריכה לנווט שמאלה. כדי להציג את שורת הכותרת, נעשה שימוש בפונקציה הבאה:

def display_heading_line (מסגרת, ציר היגוי, line_color = (0, 0, 255), line_width = 5)

כותרת_תמונה = np.zeros_like (מסגרת) גובה, רוחב, _ = frame.shape היגוי_אנגל_רדיאן = היגוי_אנגל / 180.0 * math.pi x1 = int (רוחב / 2) y1 = גובה x2 = int (x1 - גובה / 2 / מתמטיקה. טאן (Steering_angle_radian)) y2 = int (גובה / 2) cv2.line (כותרת_תמונה, (x1, y1), (x2, y2), קו_צבע, קו_ רוחב) heading_image = cv2.addWeighted (מסגרת, 0.8, כותרת_תמונה, 1, 1) חזור כותרת_תמונה

הפונקציה שלמעלה לוקחת את המסגרת שבה יישרטט קו הכותרת וזווית ההיגוי כקלט. הוא מחזיר את התמונה של שורת הכותרת. מסגרת קו הכותרת שצולמה במקרה שלי מוצגת בתמונה למעלה.

שילוב כל הקוד ביחד:

הקוד כעת מוכן להרכבה. הקוד הבא מציג את הלולאה הראשית של התוכנית שקוראת לכל פונקציה:

יבוא cv2

ייבא numpy כ- np video = cv2. VideoCapture (0) video.set (cv2. CAP_PROP_FRAME_WIDTH, 320) video.set (cv2. CAP_PROP_FRAME_HEIGHT, 240) בעוד True: ret, frame = video.read () frame = cv2.flip (מסגרת, -1) #קריאת הפונקציות hsv = convert_to_HSV (frame) edge = detect_edges (hsv) roi = region_of_interest (edge) line_segments = detect_line_segments (roi) lane_lines = average_slope_intercept (frame, line_segments) lane_lines_image = display_lines (frame_lines) = get_steering_angle (frame, lane_lines) heading_image = display_heading_line (lane_lines_image, steering_angle) key = cv2.waitKey (1) if key == 27: break video.release () cv2.destroyAllWindows ()

שלב 6: החלת בקרת PD

החלת בקרת PD
החלת בקרת PD

עכשיו יש לנו את זווית ההיגוי שלנו מוכנה להזנה למנועים. כפי שצוין קודם לכן, אם זווית ההיגוי גדולה מ- 90, המכונית צריכה לפנות ימינה אחרת היא צריכה לפנות שמאלה. יישמתי קוד פשוט שמפנה את מנוע ההיגוי ימינה אם הזווית מעל 90 והופך אותו שמאלה אם זווית ההיגוי נמוכה מ -90 במהירות מצערת קבועה של (10% PWM) אך קיבלתי הרבה טעויות. השגיאה העיקרית שקיבלתי היא כאשר המכונית מתקרבת לכל סיבוב, מנוע ההיגוי פועל ישירות אך מנועי החנק נתקעים. ניסיתי להגדיל את מהירות המצערת להיות (20% PWM) בסיבובים אך הסתיים בכך שהרובוט יצא מהנתיבים. הייתי צריך משהו שמגדיל מאוד את מהירות המצערת אם זווית ההיגוי גדולה מאוד ומגדילה מעט את המהירות אם זווית ההיגוי לא כל כך גדולה ואז מורידה את המהירות לערך התחלתי כשהמכונית מתקרבת ל 90 מעלות (נעה ישר). הפתרון היה שימוש בבקר PD.

בקר PID מייצג בקר פרופורציונלי, אינטגרלי ונגזרת. סוג זה של בקרים ליניאריים נמצא בשימוש נרחב ביישומי רובוטיקה. התמונה למעלה מציגה את לולאת בקרת המשוב האופיינית ל- PID. מטרתו של בקר זה היא להגיע ל"נקודת החיתוך "בצורה היעילה ביותר בניגוד לבקרי" הדלקה "המפעילים או מכבים את המפעל בהתאם לתנאים מסוימים. כמה מילות מפתח צריכות להיות ידועות:

  • נקודת ערך: הוא הערך הרצוי שאליו תרצה שהמערכת שלך תגיע.
  • ערך בפועל: הוא הערך האמיתי שחוש על ידי חיישן.
  • שגיאה: הוא ההבדל בין ערך נקוב לערך בפועל (error = Setpoint - ערך בפועל).
  • משתנה מבוקר: משמו, המשתנה עליו ברצונך לשלוט.
  • Kp: קבוע יחסי.
  • Ki: קבוע אינטגרלי.
  • Kd: קבוע נגזר.

בקיצור, לולאת מערכת הבקרה PID פועלת כדלקמן:

  • המשתמש מגדיר את נקודת ההתחלה הדרושה כדי שהמערכת תגיע.
  • השגיאה מחושבת (שגיאה = נקודת ערך - בפועל).
  • בקר P יוצר פעולה ביחס לערך השגיאה. (השגיאה עולה, גם פעולת P עולה)
  • בקר I ישלב את השגיאה לאורך זמן מה שמבטל את שגיאת המצב היציב של המערכת אך מגביר את הצפיפות שלה.
  • בקר D הוא פשוט נגזרת הזמן לשגיאה. במילים אחרות, זהו שיפוע השגיאה. הוא עושה פעולה ביחס לנגזרת השגיאה. בקר זה מגביר את יציבות המערכת.
  • פלט הבקר יהיה סכום שלושת הבקרים. פלט הבקר יהפוך ל -0 אם השגיאה תהיה 0.

הסבר נהדר על בקר PID ניתן למצוא כאן.

כשחזרתי למכונית השמירה על הנתיב, המשתנה הנשלט שלי היה מהירות מצערת (מכיוון שלהיגוי יש שתי מצבים בלבד ימין או שמאל). בקר PD משמש למטרה זו מכיוון שפעולת D מגבירה מאוד את מהירות החנק אם שינוי השגיאה גדול מאוד (כלומר סטייה גדולה) ומאט את המכונית אם שינוי השגיאה הזה מתקרב ל 0. עשיתי את השלבים הבאים ליישום PD בקר:

  • הגדר את נקודת ההגדרה ל- 90 מעלות (אני תמיד רוצה שהרכב ינוע ישר)
  • חישב את זווית הסטייה מהאמצע
  • הסטייה נותנת שני מידע: עד כמה גדולה השגיאה (גודל הסטייה) ולאיזה כיוון מנוע ההיגוי צריך לפנות (סימן לסטייה). אם הסטייה חיובית, המכונית צריכה לנווט ימינה אחרת היא צריכה לנווט שמאלה.
  • מכיוון שהסטייה היא שלילית או חיובית, משתנה "שגיאה" מוגדר ותמיד שווה לערך המוחלט של הסטייה.
  • השגיאה מוכפלת ב- Kp קבוע.
  • השגיאה עוברת בידול בזמן ומוכפלת ב- Kd קבוע.
  • מהירות המנוע מתעדכנת והלולאה מתחילה מחדש.

הקוד הבא משמש בלולאה הראשית לשליטה על מהירות מנועי החנק:

מהירות = 10 # מהירות הפעלה ב- % PWM

# משתנים שיש לעדכן כל לולאה lastTime = 0 lastError = 0 # קבועי PD Kp = 0.4 Kd = Kp * 0.65 בעוד נכון: עכשיו = time.time () # משתנה זמן הנוכחי dt = now - סטיית LastTime = steering_angle - 90 # שווה ערך to error_to_mid_deg שגיאה משתנה = abs (סטייה) אם סטייה -5: # אל תנווט אם יש סטיית טווח שגיאות של 10 מעלות = 0 שגיאה = 0 GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. LOW) steering.stop () סטיית elif> 5: # היכו ימינה אם הסטייה חיובית GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. HIGH) steering.start (100) סטיית elif < -5: # היכו שמאלה אם הסטייה שלילית GPIO.output (in1, GPIO. HIGH) GPIO.output (in2, GPIO. LOW) steering.start (100) נגזרת = kd * (שגיאה - lastError) / dt פרופורציונלי = kp * שגיאה PD = int (מהירות + נגזרת + פרופורציונלית) spd = abs (PD) אם spd> 25: spd = 25 throttle.start (spd) lastError = error lastTime = time.time ()

אם השגיאה גדולה מאוד (החריגה מהאמצע גבוהה), הפעולות הפרופורציות והנגזרות גבוהות וכתוצאה מכך מהירות חנק גבוהה. כאשר השגיאה מתקרבת ל -0 (הסטייה מהאמצע נמוכה), הפעולה הנגזרת פועלת הפוך (השיפוע שלילי) ומהירות החנקה נמוכה כדי לשמור על יציבות המערכת. הקוד המלא מצורף למטה.

שלב 7: תוצאות

הסרטונים למעלה מראים את התוצאות שהשגתי. הוא אכן דורש כוונון נוסף והתאמות נוספות. חיברתי את הפטל פטל למסך תצוגת ה- LCD שלי מכיוון שהזרמת הווידיאו דרך הרשת שלי הייתה בעלת חביון גבוה והיה מאוד מתסכל לעבוד איתו, בגלל זה יש חוטים המחוברים לפטל פאי בסרטון. השתמשתי בלוחות קצף כדי לצייר את המסלול.

אני מחכה לשמוע את ההמלצות שלך כדי להפוך את הפרויקט לטוב יותר! כפי שאני מקווה שמדריך זה היה מספיק טוב כדי לתת לך מידע חדש.

מוּמלָץ: