สารบัญ:

รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV: 7 ขั้นตอน (พร้อมรูปภาพ)
รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV: 7 ขั้นตอน (พร้อมรูปภาพ)

วีดีโอ: รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV: 7 ขั้นตอน (พร้อมรูปภาพ)

วีดีโอ: รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV: 7 ขั้นตอน (พร้อมรูปภาพ)
วีดีโอ: สอนประยุกต์ใช้ Raspberry Pi กับ Arduino UNO ร่วมกับ กล้อง Pi Camera เพื่อทำโครงงานหุ่นยนต์เบื้องต้น 2024, พฤศจิกายน
Anonim
รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV
รถรักษาช่องทางเดินรถอัตโนมัติโดยใช้ Raspberry Pi และ OpenCV

ในคำแนะนำนี้ หุ่นยนต์รักษาช่องจราจรอัตโนมัติจะถูกนำมาใช้และจะผ่านขั้นตอนต่อไปนี้:

  • รวบรวมชิ้นส่วน
  • ข้อกำหนดเบื้องต้นในการติดตั้งซอฟต์แวร์
  • การประกอบฮาร์ดแวร์
  • การทดสอบครั้งแรก
  • การตรวจจับช่องทางเดินรถและการแสดงเส้นบอกแนวโดยใช้ openCV
  • การใช้ตัวควบคุม PD
  • ผลลัพธ์

ขั้นตอนที่ 1: รวบรวมส่วนประกอบ

รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ
รวบรวมส่วนประกอบ

ภาพด้านบนแสดงส่วนประกอบทั้งหมดที่ใช้ในโครงการนี้:

  • รถ RC: ฉันได้รับของฉันจากร้านค้าในประเทศของฉัน มีมอเตอร์ 3 ตัว (2 สำหรับการควบคุมปริมาณและ 1 ตัวสำหรับการบังคับเลี้ยว) ข้อเสียเปรียบหลักของรถคันนี้คือ การบังคับเลี้ยวถูกจำกัดระหว่าง "ไม่มีการบังคับเลี้ยว" กับ "การบังคับเลี้ยวเต็ม" กล่าวอีกนัยหนึ่งคือไม่สามารถบังคับทิศทางในมุมเฉพาะได้ซึ่งแตกต่างจากรถ RC แบบบังคับด้วยเซอร์โว คุณสามารถค้นหาชุดอุปกรณ์ในรถที่คล้ายกันซึ่งออกแบบมาเป็นพิเศษสำหรับ Raspberry Pi ได้จากที่นี่
  • Raspberry pi 3 รุ่น b+: นี่คือสมองของรถที่จะจัดการกับขั้นตอนการประมวลผลจำนวนมาก มันใช้โปรเซสเซอร์ Quad Core 64 บิตที่โอเวอร์คล็อกที่ 1.4 GHz ฉันได้ของฉันจากที่นี่
  • โมดูลกล้อง Raspberry pi 5 mp: รองรับการบันทึก 1080p @ 30 fps, 720p @ 60 fps และ 640x480p 60/90 นอกจากนี้ยังรองรับอินเทอร์เฟซแบบอนุกรมซึ่งสามารถเสียบเข้ากับ Raspberry Pi ได้โดยตรง ไม่ใช่ตัวเลือกที่ดีที่สุดสำหรับแอปพลิเคชันการประมวลผลภาพ แต่เพียงพอสำหรับโครงการนี้และราคาถูกมาก ฉันได้ของฉันจากที่นี่
  • ตัวขับมอเตอร์: ใช้เพื่อควบคุมทิศทางและความเร็วของมอเตอร์กระแสตรง รองรับการควบคุมมอเตอร์กระแสตรง 2 ตัวในบอร์ดเดียว และสามารถทนต่อ 1.5 A.
  • Power Bank (ไม่บังคับ): ฉันใช้พาวเวอร์แบงค์ (พิกัด 5V, 3A) เพื่อเพิ่มพลังให้ Raspberry pi แยกกัน ควรใช้ตัวแปลงสเต็ปดาวน์ (ตัวแปลงบั๊ก: กระแสเอาต์พุต 3A) เพื่อเพิ่มพลังให้กับราสเบอร์รี่ pi จากแหล่งเดียว
  • แบตเตอรี่ LiPo 3 วินาที (12 V): แบตเตอรี่ลิเธียมโพลิเมอร์เป็นที่รู้จักในด้านประสิทธิภาพที่ยอดเยี่ยมในด้านวิทยาการหุ่นยนต์ ใช้สำหรับจ่ายไฟให้กับตัวขับมอเตอร์ ฉันซื้อของฉันจากที่นี่
  • สายจัมเปอร์ตัวผู้กับตัวผู้ และตัวเมียกับตัวเมีย
  • เทปสองหน้า: ใช้สำหรับติดส่วนประกอบบนรถ RC
  • เทปสีน้ำเงิน: นี่เป็นองค์ประกอบที่สำคัญมากของโครงการนี้ ใช้สำหรับสร้างเลนสองเลนที่รถจะขับระหว่าง คุณสามารถเลือกสีที่ต้องการได้ แต่ฉันแนะนำให้เลือกสีที่แตกต่างจากสีในสภาพแวดล้อมโดยรอบ
  • ซิปรูดและแท่งไม้
  • ไขควง.

ขั้นตอนที่ 2: การติดตั้ง OpenCV บน Raspberry Pi และการตั้งค่า Remote Display

การติดตั้ง OpenCV บน Raspberry Pi และการตั้งค่า Remote Display
การติดตั้ง OpenCV บน Raspberry Pi และการตั้งค่า Remote Display

ขั้นตอนนี้ค่อนข้างน่ารำคาญและจะใช้เวลาสักครู่

OpenCV (โอเพ่นซอร์ส Computer Vision) เป็นวิสัยทัศน์คอมพิวเตอร์โอเพ่นซอร์สและไลบรารีซอฟต์แวร์การเรียนรู้ของเครื่อง ห้องสมุดมีอัลกอริธึมที่ปรับให้เหมาะสมกว่า 2,500 รายการ ทำตามคำแนะนำที่ตรงไปตรงมานี้เพื่อติดตั้ง openCV บน raspberry pi ของคุณ เช่นเดียวกับการติดตั้ง raspberry pi OS (หากคุณยังไม่ได้ทำ) โปรดทราบว่าขั้นตอนในการสร้าง openCV อาจใช้เวลาประมาณ 1.5 ชั่วโมงในห้องที่มีอากาศเย็น (เนื่องจากอุณหภูมิของโปรเซสเซอร์จะสูงมาก!) ดังนั้นควรดื่มชาและอดทนรอ:D

สำหรับการแสดงผลระยะไกล ให้ปฏิบัติตามคำแนะนำนี้เพื่อตั้งค่าการเข้าถึงราสเบอร์รี่ pi ของคุณจากอุปกรณ์ Windows/Mac ของคุณ

ขั้นตอนที่ 3: เชื่อมต่อชิ้นส่วนต่างๆ เข้าด้วยกัน

เชื่อมต่อชิ้นส่วนเข้าด้วยกัน
เชื่อมต่อชิ้นส่วนเข้าด้วยกัน
เชื่อมต่อชิ้นส่วนเข้าด้วยกัน
เชื่อมต่อชิ้นส่วนเข้าด้วยกัน
เชื่อมต่อชิ้นส่วนเข้าด้วยกัน
เชื่อมต่อชิ้นส่วนเข้าด้วยกัน

ภาพด้านบนแสดงการเชื่อมต่อระหว่าง Raspberry Pi โมดูลกล้อง และไดรเวอร์มอเตอร์ โปรดทราบว่ามอเตอร์ที่ฉันใช้ดูดซับ 0.35 A ที่ 9 V แต่ละตัว ซึ่งทำให้ตัวขับมอเตอร์ทำงาน 3 มอเตอร์พร้อมกันได้อย่างปลอดภัย และเนื่องจากฉันต้องการควบคุมความเร็วของมอเตอร์ควบคุมปริมาณ 2 ตัว (ด้านหลัง 1 ตัวและด้านหน้า 1 ตัว) ในลักษณะเดียวกันทุกประการ ฉันจึงเชื่อมต่อมันเข้ากับพอร์ตเดียวกัน ฉันติดตั้งตัวขับมอเตอร์ที่ด้านขวาของรถโดยใช้เทปกาวสองชั้น สำหรับโมดูลกล้องนั้น ฉันได้ใส่ซิปผูกระหว่างรูสกรูดังที่แสดงไว้ด้านบน จากนั้นจึงใส่กล้องเข้ากับแท่งไม้เพื่อปรับตำแหน่งของกล้องได้ตามต้องการ พยายามติดตั้งกล้องไว้ตรงกลางรถให้มากที่สุด ผมแนะนำให้วางกล้องไว้เหนือพื้นอย่างน้อย 20 ซม. เพื่อให้ระยะการมองเห็นด้านหน้ารถดีขึ้น แผนผัง Fritzing แนบมาด้านล่าง

ขั้นตอนที่ 4: การทดสอบครั้งแรก

การทดสอบครั้งแรก
การทดสอบครั้งแรก
การทดสอบครั้งแรก
การทดสอบครั้งแรก

การทดสอบกล้อง:

เมื่อติดตั้งกล้องแล้ว และสร้างไลบรารี่ openCV ก็ถึงเวลาทดสอบภาพแรกของเรา! เราจะถ่ายรูปจาก pi cam และบันทึกเป็น "original.jpg" สามารถทำได้ 2 วิธี คือ

1. การใช้คำสั่งเทอร์มินัล:

เปิดหน้าต่างเทอร์มินัลใหม่และพิมพ์คำสั่งต่อไปนี้:

raspistill -o original.jpg

การดำเนินการนี้จะถ่ายภาพนิ่งและบันทึกไว้ในไดเร็กทอรี "/pi/original.jpg"

2. การใช้ python IDE ใด ๆ (ฉันใช้ IDLE):

เปิดร่างใหม่และเขียนโค้ดต่อไปนี้:

นำเข้า cv2

video = cv2. VideoCapture(0) while True: ret, frame = video.read() frame = cv2.flip(frame, -1) # ใช้เพื่อพลิกภาพในแนวตั้ง cv2.imshow('original', frame) cv2. imwrite ('original.jpg', เฟรม) คีย์ = cv2.waitKey(1) ถ้าคีย์ == 27: ทำลาย video.release () cv2.destroyAllWindows ()

มาดูกันว่าเกิดอะไรขึ้นในรหัสนี้ บรรทัดแรกกำลังนำเข้าไลบรารี openCV ของเราเพื่อใช้ฟังก์ชันทั้งหมด ฟังก์ชัน VideoCapture(0) เริ่มสตรีมวิดีโอสดจากแหล่งที่มาที่กำหนดโดยฟังก์ชันนี้ ในกรณีนี้คือ 0 ซึ่งหมายถึงกล้อง raspi หากคุณมีกล้องหลายตัว ควรวางตัวเลขต่างกัน video.read() จะอ่านแต่ละเฟรมที่มาจากกล้องและบันทึกลงในตัวแปรที่เรียกว่า "frame" ฟังก์ชั่น flip() จะพลิกภาพตามแกน y (แนวตั้ง) เนื่องจากฉันกำลังติดตั้งกล้องกลับด้าน imshow() จะแสดงเฟรมของเราที่มีคำว่า "ต้นฉบับ" และ imwrite() จะบันทึกรูปภาพของเราเป็นต้นฉบับ-j.webp

ฉันแนะนำให้ทดสอบรูปภาพของคุณด้วยวิธีที่สองเพื่อทำความคุ้นเคยกับฟังก์ชัน openCV รูปภาพจะถูกบันทึกไว้ในไดเร็กทอรี "/pi/original.jpg" รูปภาพต้นฉบับที่กล้องของฉันถ่ายแสดงไว้ด้านบน

มอเตอร์ทดสอบ:

ขั้นตอนนี้มีความสำคัญต่อการกำหนดทิศทางการหมุนของมอเตอร์แต่ละตัว อันดับแรก เรามาแนะนำสั้น ๆ เกี่ยวกับหลักการทำงานของตัวขับมอเตอร์กัน ภาพด้านบนแสดงพินเอาต์ของไดรเวอร์มอเตอร์ เปิดใช้งาน A อินพุต 1 และอินพุต 2 เชื่อมโยงกับการควบคุมมอเตอร์ A เปิดใช้งาน B อินพุต 3 และอินพุต 4 เชื่อมโยงกับการควบคุมมอเตอร์ B การควบคุมทิศทางถูกสร้างขึ้นโดยส่วน "อินพุต" และการควบคุมความเร็วถูกสร้างขึ้นโดยส่วน "เปิดใช้งาน" ในการควบคุมทิศทางของมอเตอร์ A เช่น ตั้งค่า Input 1 เป็น HIGH (3.3 V ในกรณีนี้ เนื่องจากเราใช้ Raspberry Pi) และตั้งค่า Input 2 เป็น LOW มอเตอร์จะหมุนไปในทิศทางที่กำหนดและโดยการตั้งค่าที่ตรงกันข้าม สำหรับอินพุต 1 และอินพุต 2 มอเตอร์จะหมุนไปในทิศทางตรงกันข้าม หากอินพุต 1 = อินพุต 2 = (สูงหรือต่ำ) มอเตอร์จะไม่หมุน เปิดใช้งานพินใช้สัญญาณอินพุต Pulse Width Modulation (PWM) จากราสเบอร์รี่ (0 ถึง 3.3 V) และเรียกใช้มอเตอร์ตามลำดับ ตัวอย่างเช่น สัญญาณ PWM 100% หมายความว่าเรากำลังทำงานกับความเร็วสูงสุดและสัญญาณ PWM 0% หมายถึงมอเตอร์ไม่หมุน รหัสต่อไปนี้ใช้เพื่อกำหนดทิศทางของมอเตอร์และทดสอบความเร็ว

เวลานำเข้า

นำเข้า RPi. GPIO เป็น GPIO GPIO.setwarnings (False) # หมุดมอเตอร์บังคับเลี้ยว steering_enable = 22 # พินกายภาพ 15 in1 = 17 # พินกายภาพ 11 in2 = 27 # พินกายภาพ 13 # พินมอเตอร์คันเร่ง throttle_enable = 25 # พินกายภาพ 22 in3 = 23 # พินฟิสิคัล 16 in4 = 24 # พินฟิสิคัล 18 GPIO.setmode (GPIO. BCM) # ใช้การกำหนดหมายเลข GPIO แทนการกำหนดหมายเลขจริง GPIO.setup (in1, GPIO.out) GPIO.setup (in2, GPIO.out) GPIO ตั้งค่า (in3, GPIO.out) GPIO.setup (in4, GPIO.out) GPIO.setup (throttle_enable, GPIO.out) GPIO.setup (steering_enable, GPIO.out) # ควบคุมมอเตอร์ควบคุม GPIO.output (in1, GPIO. สูง) GPIO.output(in2, GPIO. LOW) พวงมาลัย = GPIO. PWM(steering_enable, 1000) # ตั้งค่าความถี่การสลับเป็น 1000 Hz พวงมาลัยหยุด () # การควบคุมมอเตอร์คันเร่ง GPIO.output (in3, GPIO. HIGH) GPIO.output(in4, GPIO. LOW) throttle = GPIO. PWM(throttle_enable, 1000) # ตั้งค่าความถี่การสลับเป็น 1000 Hz throttle.stop() time.sleep(1) throttle.start(25) # สตาร์ทมอเตอร์ที่ 25 % สัญญาณ PWM-> (0.25 * แรงดันแบตเตอรี่) - ไดรเวอร์ สูญเสียการบังคับเลี้ยว สตาร์ท (100) # สตาร์ทมอเตอร์ที่สัญญาณ PWM 100%-> (1 * แรงดันแบตเตอรี่) - เวลาสูญเสียของคนขับ สลีป(3) throttle.stop() steering.stop()

รหัสนี้จะเรียกใช้มอเตอร์ควบคุมปริมาณและมอเตอร์บังคับเลี้ยวเป็นเวลา 3 วินาที จากนั้นจะหยุดทำงาน สามารถกำหนด (การสูญเสียของคนขับ) ได้โดยใช้โวลต์มิเตอร์ ตัวอย่างเช่น เรารู้ว่าสัญญาณ PWM 100% ควรให้แรงดันไฟฟ้าของแบตเตอรี่เต็มที่ที่ขั้วของมอเตอร์ แต่ด้วยการตั้งค่า PWM เป็น 100% ฉันพบว่าไดรเวอร์ทำให้ 3 V ลดลงและมอเตอร์ได้รับ 9 V แทนที่จะเป็น 12 V (ตรงตามที่ฉันต้องการ!) การสูญเสียไม่เป็นเชิงเส้น กล่าวคือ การสูญเสียที่ 100% แตกต่างจากการสูญเสียที่ 25% อย่างมาก หลังจากรันโค้ดด้านบนแล้ว ผลลัพธ์ของฉันมีดังนี้:

ผลลัพธ์ของการควบคุมปริมาณ: ถ้า in3 = สูง และ in4 = LOW มอเตอร์ควบคุมปริมาณจะมีการหมุนตามเข็มนาฬิกา (CW) เช่น รถจะเคลื่อนที่ไปข้างหน้า มิฉะนั้นรถจะเคลื่อนถอยหลัง

ผลการบังคับเลี้ยว: ถ้า in1 = HIGH และ in2 = LOW มอเตอร์บังคับเลี้ยวจะเลี้ยวซ้ายสุด นั่นคือ รถจะเลี้ยวซ้าย มิฉะนั้นรถจะเลี้ยวขวา หลังจากการทดลองบางอย่าง ฉันพบว่ามอเตอร์บังคับเลี้ยวจะไม่หมุนหากสัญญาณ PWM ไม่ 100% (เช่น มอเตอร์จะหมุนไปทางขวาเต็มที่หรือไปทางซ้ายเต็มที่)

ขั้นตอนที่ 5: การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า

การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า
การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า
การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า
การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า
การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า
การตรวจจับเส้นเลนและการคำนวณเส้นมุ่งหน้า

ในขั้นตอนนี้ จะอธิบายอัลกอริทึมที่จะควบคุมการเคลื่อนที่ของรถ ภาพแรกแสดงกระบวนการทั้งหมด อินพุตของระบบคือภาพ เอาต์พุตคือทีต้า (มุมบังคับเลี้ยวเป็นองศา) โปรดทราบว่าการประมวลผลเสร็จสิ้นใน 1 ภาพและจะทำซ้ำในทุกเฟรม

กล้อง:

กล้องจะเริ่มบันทึกวิดีโอด้วยความละเอียด (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 # The loop while จริง: 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 Color Space:

หลังจากถ่ายวิดีโอเป็นเฟรมจากกล้องแล้ว ขั้นตอนต่อไปคือการแปลงแต่ละเฟรมเป็นพื้นที่สี Hue, Saturation และ Value (HSV) ข้อได้เปรียบหลักของการทำเช่นนี้คือสามารถแยกความแตกต่างระหว่างสีตามระดับความสว่างของสีได้ และนี่คือคำอธิบายที่ดีเกี่ยวกับปริภูมิสี HSV การแปลงเป็น HSV ทำได้โดยใช้ฟังก์ชันต่อไปนี้:

def แปลง_to_HSV (เฟรม):

hsv = cv2.cvtColor (เฟรม cv2. COLOR_BGR2HSV) cv2.imshow ("HSV", hsv) ส่งคืน hsv

ฟังก์ชันนี้จะถูกเรียกจากลูปหลักและจะคืนค่าเฟรมในปริภูมิสี HSV เฟรมที่ฉันได้รับในปริภูมิสี HSV แสดงไว้ด้านบน

ตรวจจับสีน้ำเงินและขอบ:

หลังจากแปลงภาพเป็นพื้นที่สี HSV ก็ถึงเวลาที่จะตรวจจับเฉพาะสีที่เราสนใจ (เช่น สีฟ้าเนื่องจากเป็นสีของเส้นช่องจราจร) หากต้องการแยกสีน้ำเงินออกจากกรอบ HSV ควรระบุช่วงของเฉดสี ความอิ่มตัว และค่า อ้างถึงที่นี่เพื่อให้มีแนวคิดที่ดีขึ้นเกี่ยวกับค่า HSV หลังจากการทดลองบางอย่าง ขีดจำกัดบนและล่างของสีน้ำเงินจะแสดงอยู่ในโค้ดด้านล่าง และเพื่อลดความผิดเพี้ยนโดยรวมในแต่ละเฟรม ขอบจะถูกตรวจจับโดยใช้ตัวตรวจจับขอบคมเท่านั้น ดูข้อมูลเพิ่มเติมเกี่ยวกับ canny edge ได้ที่นี่ กฎทั่วไปคือการเลือกพารามิเตอร์ของฟังก์ชัน Canny() ด้วยอัตราส่วน 1:2 หรือ 1:3

def detect_edges (เฟรม):

lower_blue = 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):

การเลือกพื้นที่ที่สนใจเป็นสิ่งสำคัญที่จะต้องเน้นที่ 1 ส่วนของเฟรมเท่านั้น ในกรณีนี้ ผมไม่อยากให้รถเห็นสิ่งของมากมายในสิ่งแวดล้อม ฉันแค่ต้องการให้รถโฟกัสที่เส้นช่องจราจรและไม่สนใจอย่างอื่น PS: ระบบพิกัด (แกน x และ y) เริ่มจากมุมซ้ายบน กล่าวอีกนัยหนึ่ง จุด (0, 0) เริ่มจากมุมซ้ายบน แกน y คือความสูง และแกน x คือความกว้าง รหัสด้านล่างเลือกพื้นที่ที่สนใจเพื่อเน้นเฉพาะครึ่งล่างของเฟรม

def region_of_interest(ขอบ):

ความสูง, ความกว้าง = ขอบ. รูปร่าง # แยกความสูงและความกว้างของกรอบขอบ = np.zeros_like (ขอบ) # สร้างเมทริกซ์ว่างที่มีขนาดเท่ากันของกรอบขอบ # เน้นเฉพาะครึ่งล่างของหน้าจอ # ระบุพิกัดของ 4 จุด (ซ้ายล่าง ซ้ายบน ขวาบน ขวาล่าง) รูปหลายเหลี่ยม = np.array(

ฟังก์ชันนี้จะใช้กรอบที่มีขอบเป็นพารามิเตอร์ และวาดรูปหลายเหลี่ยมที่มีจุดที่กำหนดไว้ล่วงหน้า 4 จุด มันจะเน้นเฉพาะสิ่งที่อยู่ภายในรูปหลายเหลี่ยมและไม่สนใจทุกสิ่งที่อยู่ภายนอก กรอบขอบเขตความสนใจของฉันแสดงไว้ด้านบน

ตรวจหาส่วนของเส้น:

การแปลง Hough ใช้เพื่อตรวจจับส่วนของเส้นตรงจากกรอบที่มีขอบ การแปลง Hough เป็นเทคนิคในการตรวจจับรูปร่างใดๆ ในรูปแบบทางคณิตศาสตร์ มันสามารถตรวจจับวัตถุได้เกือบทุกชนิดแม้ว่าจะบิดเบี้ยวตามจำนวนโหวต ข้อมูลอ้างอิงที่ดีสำหรับการแปลง Hough แสดงไว้ที่นี่ สำหรับแอปพลิเคชันนี้ ฟังก์ชัน cv2. HoughLinesP() ใช้เพื่อตรวจจับเส้นในแต่ละเฟรม พารามิเตอร์ที่สำคัญของฟังก์ชันนี้คือ:

cv2. HoughLinesP(เฟรม, rho, theta, min_threshold, minLineLength, maxLineGap)

  • Frame: เป็นเฟรมที่เราต้องการตรวจจับเส้นเข้า
  • rho: เป็นความแม่นยำของระยะทางเป็นพิกเซล (ปกติคือ = 1)
  • theta: ความแม่นยำเชิงมุมในหน่วยเรเดียน (เสมอ = np.pi/180 ~ 1 องศา)
  • min_threshold: คะแนนขั้นต่ำที่ควรได้รับเพื่อให้ได้รับการพิจารณาเป็นบรรทัด
  • minLineLength: ความยาวขั้นต่ำของบรรทัดเป็นพิกเซล เส้นที่สั้นกว่าตัวเลขนี้ไม่ถือเป็นบรรทัด
  • maxLineGap: ช่องว่างสูงสุดในหน่วยพิกเซลระหว่าง 2 บรรทัดให้ถือว่าเป็น 1 บรรทัด (กรณีของฉันไม่ได้ใช้เพราะเลนที่ฉันใช้ไม่มีช่องว่าง)

ฟังก์ชันนี้ส่งคืนจุดสิ้นสุดของบรรทัด ฟังก์ชั่นต่อไปนี้ถูกเรียกจากลูปหลักของฉันเพื่อตรวจจับเส้นโดยใช้ Hough transform:

def detect_line_segments (ครอบตัด_ขอบ):

rho = 1 theta = np.pi / 180 min_threshold = 10 line_segments = cv2. HoughLinesP(cropped_edges, rho, theta, min_threshold, np.array(), minLineLength=5, maxLineGap=0) return line_segments

ความชันและการสกัดกั้นเฉลี่ย (m, b):

จำได้ว่าสมการเส้นตรงถูกกำหนดโดย y = mx + b โดยที่ m คือความชันของเส้นตรง และ b คือค่าตัดแกน y ในส่วนนี้ ค่าเฉลี่ยของความชันและจุดตัดของส่วนของเส้นตรงที่ตรวจพบโดยใช้การแปลง Hough จะถูกคำนวณ ก่อนดำเนินการดังกล่าว มาดูรูปภาพในกรอบเดิมที่แสดงไว้ด้านบนกันก่อน เลนซ้ายดูเหมือนจะขึ้นไปด้านบนดังนั้นจึงมีความชันเป็นลบ (จำจุดเริ่มต้นระบบพิกัดได้หรือไม่) กล่าวอีกนัยหนึ่ง เส้นเลนซ้ายมี x1 < x2 และ y2 x1 และ y2 > y1 ซึ่งจะให้ความชันเป็นบวก ดังนั้น เส้นทั้งหมดที่มีความชันเป็นบวกจะถือเป็นจุดเลนขวา ในกรณีของเส้นแนวตั้ง (x1 = x2) ความชันจะเป็นอนันต์ ในกรณีนี้ เราจะข้ามเส้นแนวตั้งทั้งหมดเพื่อป้องกันไม่ให้เกิดข้อผิดพลาด เพื่อเพิ่มความแม่นยำให้กับการตรวจจับนี้ แต่ละเฟรมจะถูกแบ่งออกเป็นสองส่วน (ขวาและซ้าย) ผ่าน 2 ขอบเขต จุดความกว้างทั้งหมด (จุดแกน x) ที่มากกว่าเส้นเขตแดนด้านขวา จะสัมพันธ์กับการคำนวณเลนขวา และถ้าจุดความกว้างทั้งหมดน้อยกว่าเส้นเขตด้านซ้าย จะสัมพันธ์กับการคำนวณเลนซ้าย ฟังก์ชันต่อไปนี้ใช้เฟรมที่อยู่ระหว่างการประมวลผลและส่วนของเลนที่ตรวจพบโดยใช้การแปลง Hough และคืนค่าความชันเฉลี่ยและการสกัดกั้นของเส้นสองเลน

def เฉลี่ย_slope_intercept(เฟรม, line_segments):

lane_lines = ถ้า line_segments ไม่มี: พิมพ์ ("ไม่พบส่วนของเส้นตรง") ส่งคืนความสูง lane_lines ความกว้าง _ = frame.shape left_fit = right_fit = ขอบเขต = left_region_boundary = width * (1 - ขอบเขต) right_region_boundary = ความกว้าง * ขอบเขตสำหรับ line_segment ใน line_segments: สำหรับ x1, y1, x2, y2 ใน line_segment: if x1 == x2: print("การข้ามเส้นแนวตั้ง (ความชัน = อินฟินิตี้)") ดำเนินการต่อพอดี = np.polyfit((x1, x2), (y1, y2), 1) ความชัน = (y2 - y1) / (x2 - x1) จุดตัด = y1 - (ความชัน * x1) ถ้าความชัน < 0: ถ้า x1 < left_region_boundary และ x2 right_region_boundary and x2 > right_region_boundary: right_fit ผนวก((ลาด, สกัดกั้น)) 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 เป็นอาร์เรย์ 2 มิติที่ประกอบด้วยพิกัดของเส้นเลนขวาและซ้าย # ตัวอย่างเช่น: 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(frame) ถ้าเส้นไม่ใช่ไม่มี: สำหรับบรรทัดในบรรทัด: สำหรับ x1, y1, x2, y2 ในบรรทัด: cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width) line_image = cv2.addWeighted (เฟรม 0.8, line_image, 1, 1) ส่งคืน line_image

ฟังก์ชัน cv2.addWeighted() ใช้พารามิเตอร์ต่อไปนี้และใช้เพื่อรวมสองภาพ แต่ให้น้ำหนักแก่แต่ละภาพ

cv2.addWeighted(image1, alpha, image2, beta, แกมมา)

และคำนวณภาพที่ส่งออกโดยใช้สมการต่อไปนี้:

เอาต์พุต = อัลฟา * image1 + เบต้า * image2 + แกมมา

ข้อมูลเพิ่มเติมเกี่ยวกับฟังก์ชัน cv2.addWeighted() ได้มาจากที่นี่

คำนวณและแสดงบรรทัดหัวเรื่อง:

นี่เป็นขั้นตอนสุดท้ายก่อนที่เราจะใช้ความเร็วกับมอเตอร์ของเรา เส้นที่มุ่งหน้าไปมีหน้าที่กำหนดทิศทางของมอเตอร์บังคับเลี้ยวที่มันควรจะหมุนและให้ความเร็วของมอเตอร์ควบคุมปริมาณที่จะทำงาน การคำนวณหัวเรื่องเป็นตรีโกณมิติบริสุทธิ์ ใช้ฟังก์ชันตรีโกณมิติแทนและเอแทน (tan^-1) กรณีที่รุนแรงบางกรณีคือเมื่อกล้องตรวจพบช่องทางเดินรถเพียงเส้นเดียวหรือเมื่อตรวจไม่พบเส้นใดๆ กรณีเหล่านี้ทั้งหมดจะแสดงในฟังก์ชันต่อไปนี้:

def get_steering_angle (เฟรม lane_lines):

ความสูง, ความกว้าง, _ = frame.shape if len(lane_lines) == 2: # หากตรวจพบสองเลนไลน์ _, _, left_x2, _ = lane_lines[0][0] # extract left x2 from lane_lines array _, _, right_x2, _ = lane_lines[1][0] # แยก right x2 จาก lane_lines array mid = int(width / 2) x_offset = (left_x2 + right_x2) / 2 - mid y_offset = int(height / 2) elif len(lane_lines) == 1: # หากตรวจพบเพียงบรรทัดเดียว x1, _, x2, _ = lane_lines[0][0] x_offset = x2 - x1 y_offset = int(height / 2) elif len(lane_lines) == 0: # หากตรวจไม่พบเส้น x_offset = 0 y_offset = int(height / 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 return steering_angle

x_offset ในกรณีแรกคือค่าเฉลี่ย ((ขวา x2 + ซ้าย x2) / 2) แตกต่างจากตรงกลางหน้าจอมากน้อยเพียงใด y_offset จะถูกกำหนดให้มีความสูง / 2 เสมอ ภาพสุดท้ายด้านบนแสดงตัวอย่างบรรทัดส่วนหัว angle_to_mid_radians เหมือนกับ "theta" ที่แสดงในภาพสุดท้ายด้านบน ถ้า steering_angle = 90 แสดงว่ารถมี heading line ตั้งฉากกับเส้น "height / 2" และรถจะเคลื่อนที่ไปข้างหน้าโดยไม่มีการบังคับเลี้ยว ถ้า steering_angle > 90 รถควรเลี้ยวขวาไม่เช่นนั้นควรเลี้ยวซ้าย ในการแสดงบรรทัดหัวเรื่อง จะใช้ฟังก์ชันต่อไปนี้:

def display_heading_line (เฟรม, พวงมาลัย_angle, line_color=(0, 0, 255), line_width=5)

heading_image = np.zeros_like(frame) ความสูง ความกว้าง _ = frame.shape 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) กลับ heading_image

ฟังก์ชันด้านบนใช้เฟรมที่จะลากเส้นที่มุ่งหน้าไปและมุมบังคับเลี้ยวเป็นอินพุต มันส่งกลับภาพของบรรทัดหัวเรื่อง กรอบหัวเรื่องที่ใช้ในกรณีของฉันแสดงอยู่ในภาพด้านบน

การรวมรหัสทั้งหมดเข้าด้วยกัน:

ตอนนี้รหัสพร้อมที่จะประกอบแล้ว รหัสต่อไปนี้แสดงลูปหลักของโปรแกรมที่เรียกใช้แต่ละฟังก์ชัน:

นำเข้า 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(frame, -1) #Calling the functions hsv = convert_to_HSV(frame) edge = detect_edges(hsv) roi = region_of_interest(edges) line_segments = detect_line_segments(roi) lane_lines = average_slope_intercept(frame, line_segments) lane_lane_frames_image = get_steering_angle(frame, lane_lines) heading_image = display_heading_line(lane_lines_image, steering_angle) คีย์ = cv2.waitKey(1) if key == 27: break video.release() cv2.destroyAllWindows()

ขั้นตอนที่ 6: การใช้ PD Control

การใช้การควบคุม PD
การใช้การควบคุม PD

ตอนนี้เรามีมุมบังคับเลี้ยวที่พร้อมจะป้อนเข้าสู่มอเตอร์แล้ว ดังที่กล่าวไว้ก่อนหน้านี้ หากมุมบังคับเลี้ยวมากกว่า 90 รถควรเลี้ยวขวา ไม่เช่นนั้นควรเลี้ยวซ้าย ฉันใช้รหัสง่าย ๆ ที่จะเปลี่ยนมอเตอร์บังคับเลี้ยวให้ถูกต้องหากทำมุมเกิน 90 และเลี้ยวซ้ายหากมุมบังคับเลี้ยวน้อยกว่า 90 ที่ความเร็วการควบคุมคงที่ที่ (10% PWM) แต่พบข้อผิดพลาดมากมาย ข้อผิดพลาดหลักที่ฉันได้รับคือเมื่อรถเข้าใกล้ทางเลี้ยว มอเตอร์บังคับเลี้ยวจะทำงานโดยตรง แต่มอเตอร์ควบคุมปริมาณติดขัด ฉันพยายามเพิ่มความเร็วการควบคุมเป็น (20% PWM) เมื่อถึงคราว แต่จบลงด้วยการที่หุ่นยนต์ออกจากเลน ฉันต้องการบางอย่างที่เพิ่มความเร็วในการควบคุมปริมาณมากหากมุมบังคับเลี้ยวมีขนาดใหญ่มาก และเพิ่มความเร็วอีกเล็กน้อยหากมุมบังคับเลี้ยวไม่ใหญ่มาก จากนั้นจึงลดความเร็วเป็นค่าเริ่มต้นเมื่อรถเข้าใกล้ 90 องศา (เคลื่อนตัวตรง) วิธีแก้ไขคือใช้ตัวควบคุม PD

PID controller ย่อมาจาก Proportional, Integral และ Derivative controller ตัวควบคุมเชิงเส้นประเภทนี้ใช้กันอย่างแพร่หลายในการใช้งานหุ่นยนต์ ภาพด้านบนแสดงลูปการควบคุมป้อนกลับแบบ PID ทั่วไป เป้าหมายของตัวควบคุมนี้คือการเข้าถึง "จุดตั้งค่า" ด้วยวิธีที่มีประสิทธิภาพที่สุด ซึ่งแตกต่างจากตัวควบคุม "เปิด-ปิด" ซึ่งจะเปิดหรือปิดโรงงานตามเงื่อนไขบางประการ คำหลักบางคำควรทราบ:

  • Setpoint: คือค่าที่ต้องการที่คุณต้องการให้ระบบของคุณไปถึง
  • ค่าจริง: คือค่าจริงที่เซ็นเซอร์รับรู้
  • Error: คือความแตกต่างระหว่างค่าที่ตั้งไว้และค่าจริง (ข้อผิดพลาด = Setpoint - ค่าจริง)
  • ตัวแปรควบคุม: จากชื่อตัวแปร ตัวแปรที่คุณต้องการควบคุม
  • Kp: ค่าคงที่ตามสัดส่วน
  • Ki: ค่าคงที่ปริพันธ์
  • Kd: ค่าคงที่อนุพันธ์

กล่าวโดยย่อ ลูประบบควบคุม PID ทำงานดังนี้:

  • ผู้ใช้กำหนดการตั้งค่าที่จำเป็นสำหรับระบบในการเข้าถึง
  • มีการคำนวณข้อผิดพลาด (ข้อผิดพลาด = ค่าที่ตั้งไว้ - จริง)
  • P controller สร้างการกระทำตามสัดส่วนของค่าความผิดพลาด (ข้อผิดพลาดเพิ่มขึ้น การกระทำ P ก็เพิ่มขึ้นด้วย)
  • I ผู้ควบคุมจะรวมข้อผิดพลาดเมื่อเวลาผ่านไป ซึ่งจะขจัดข้อผิดพลาดในสถานะคงตัวของระบบ แต่เพิ่มการโอเวอร์โหลด
  • ตัวควบคุม D เป็นเพียงอนุพันธ์ของเวลาสำหรับข้อผิดพลาด กล่าวอีกนัยหนึ่งคือความชันของข้อผิดพลาด มันทำการกระทำตามสัดส่วนกับอนุพันธ์ของข้อผิดพลาด ตัวควบคุมนี้เพิ่มความเสถียรของระบบ
  • เอาต์พุตของคอนโทรลเลอร์จะเป็นผลรวมของคอนโทรลเลอร์ทั้งสามตัว เอาต์พุตของคอนโทรลเลอร์จะกลายเป็น 0 หากข้อผิดพลาดกลายเป็น 0

คุณสามารถดูคำอธิบายที่ยอดเยี่ยมของตัวควบคุม PID ได้ที่นี่

เมื่อกลับไปที่รถที่รักษาช่องจราจร ตัวแปรที่ควบคุมของฉันคือความเร็วของการควบคุม (เนื่องจากการบังคับเลี้ยวมีเพียงสองสถานะทางขวาหรือซ้าย) ตัวควบคุม PD ใช้เพื่อจุดประสงค์นี้เนื่องจากการกระทำ D จะเพิ่มความเร็วในการควบคุมปริมาณมากหากการเปลี่ยนแปลงข้อผิดพลาดมีขนาดใหญ่มาก (เช่น ค่าเบี่ยงเบนมาก) และทำให้รถช้าลงหากการเปลี่ยนแปลงข้อผิดพลาดนี้เข้าใกล้ 0 ฉันทำตามขั้นตอนต่อไปนี้เพื่อใช้ PD ตัวควบคุม:

  • ตั้งค่า setpoint ไว้ที่ 90 องศา (อยากให้รถวิ่งตรงตลอด)
  • คำนวณมุมเบี่ยงเบนจากจุดกึ่งกลาง
  • ส่วนเบี่ยงเบนให้ข้อมูลสองประการ: ข้อผิดพลาดนั้นใหญ่เพียงใด (ขนาดของส่วนเบี่ยงเบน) และทิศทางที่มอเตอร์บังคับเลี้ยวต้องใช้ (สัญญาณของการเบี่ยงเบน) ถ้าค่าเบี่ยงเบนเป็นบวก รถควรเลี้ยวขวา ไม่เช่นนั้นควรเลี้ยวซ้าย
  • เนื่องจากค่าเบี่ยงเบนเป็นค่าลบหรือค่าบวก ตัวแปร "ข้อผิดพลาด" จึงถูกกำหนดและเท่ากับค่าสัมบูรณ์ของค่าเบี่ยงเบนเสมอ
  • ข้อผิดพลาดนั้นคูณด้วยค่าคงที่ Kp
  • ข้อผิดพลาดเกิดขึ้นจากความแตกต่างของเวลาและคูณด้วยค่าคงที่ Kd
  • ความเร็วของมอเตอร์ได้รับการอัปเดตและเริ่มวนซ้ำอีกครั้ง

รหัสต่อไปนี้ใช้ในลูปหลักเพื่อควบคุมความเร็วของมอเตอร์ควบคุมปริมาณ:

ความเร็ว = 10 # ความเร็วในการทำงานเป็น% PWM

#ตัวแปรที่จะอัปเดตแต่ละลูป LastTime = 0 lastError = 0 # ค่าคงที่ PD Kp = 0.4 Kd = Kp * 0.65 ในขณะที่ True: now = time.time() # ตัวแปรเวลาปัจจุบัน dt = ตอนนี้ - ค่าเบี่ยงเบนเวลาสุดท้าย = steering_angle - 90 # เทียบเท่า เป็นข้อผิดพลาดของตัวแปร angle_to_mid_deg = abs (ส่วนเบี่ยงเบน) หากเบี่ยงเบน -5: # อย่าบังคับหากมีการเบี่ยงเบนช่วงข้อผิดพลาด 10 องศา = 0 ข้อผิดพลาด = 0 GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. LOW) พวงมาลัย. หยุด () ค่าเบี่ยงเบน elif > 5: # เลี้ยวขวาถ้าส่วนเบี่ยงเบนเป็นค่าบวก GPIO.output (in1, GPIO. LOW) GPIO.output (in2, GPIO. HIGH) พวงมาลัยพาวเวอร์สตาร์ท (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 = ข้อผิดพลาด lastTime = time.time ()

หากข้อผิดพลาดมีขนาดใหญ่มาก (ส่วนเบี่ยงเบนจากตรงกลางสูง) การกระทำตามสัดส่วนและอนุพันธ์จะสูง ส่งผลให้มีการควบคุมปริมาณความเร็วสูง เมื่อข้อผิดพลาดเข้าใกล้ 0 (ความเบี่ยงเบนจากตรงกลางมีค่าต่ำ) การกระทำอนุพันธ์จะทำหน้าที่ย้อนกลับ (ความชันเป็นลบ) และความเร็วการควบคุมจะต่ำเพื่อรักษาเสถียรภาพของระบบ รหัสเต็มที่แนบมาด้านล่าง

ขั้นตอนที่ 7: ผลลัพธ์

วิดีโอด้านบนแสดงผลลัพธ์ที่ฉันได้รับ มันต้องการการปรับแต่งเพิ่มเติมและการปรับแต่งเพิ่มเติม ฉันกำลังเชื่อมต่อ Raspberry Pi กับหน้าจอแสดงผล LCD เนื่องจากวิดีโอที่สตรีมผ่านเครือข่ายของฉันมีเวลาแฝงสูงและน่าผิดหวังมากในการทำงานด้วย นั่นคือสาเหตุที่วิดีโอมีสายไฟเชื่อมต่อกับ Raspberry Pi ฉันใช้แผ่นโฟมเพื่อวาดแทร็ก

ฉันรอฟังคำแนะนำของคุณเพื่อทำให้โครงการนี้ดีขึ้น! ฉันหวังว่าคำแนะนำนี้จะดีพอที่จะให้ข้อมูลใหม่แก่คุณได้

แนะนำ: