İçindekiler:
2025 Yazar: John Day | [email protected]. Son düzenleme: 2025-01-13 06:58
Bu talimatta, otonom bir şerit takip robotu uygulanacak ve aşağıdaki adımlardan geçecektir:
- Toplama Parçaları
- Yazılım önkoşullarını yükleme
- Donanım montajı
- İlk test
- OpenCV kullanarak şerit çizgilerini algılama ve kılavuz çizgiyi görüntüleme
- Bir PD denetleyicisini uygulama
- Sonuçlar
Adım 1: Bileşenleri Toplama
Yukarıdaki resimler bu projede kullanılan tüm bileşenleri göstermektedir:
- RC araba: Benimkini ülkemdeki yerel bir dükkandan aldım. 3 motor (2 kısma ve 1 direksiyon için) ile donatılmıştır. Bu arabanın ana dezavantajı, direksiyonun "direksiyon yok" ve "tam direksiyon" arasında sınırlı olmasıdır. Yani servo direksiyonlu RC arabaların aksine belirli bir açıda yön veremez. Raspberry pi için özel olarak tasarlanmış benzer araç kitine buradan ulaşabilirsiniz.
- Raspberry pi 3 model b+: Bu, birçok işlem aşamasını gerçekleştirecek olan arabanın beynidir. 1.4 GHz hızında çalışan dört çekirdekli 64 bit işlemciye dayanmaktadır. Benimkini buradan aldım.
- Raspberry pi 5 mp kamera modülü: 1080p @ 30 fps, 720p @ 60 fps ve 640x480p 60/90 kaydı destekler. Ayrıca doğrudan ahududu pi'ye takılabilen seri arabirimi de destekler. Görüntü işleme uygulamaları için en iyi seçenek değildir, ancak bu proje için yeterli olmasının yanı sıra çok ucuzdur. Benimkini buradan aldım.
- Motor Sürücüsü: DC motorların yön ve hızlarını kontrol etmek için kullanılır. 1 kartta 2 dc motorun kontrolünü destekler ve 1,5 A'ya dayanabilir.
- Güç Bankası (İsteğe bağlı): Ahududu pi'yi ayrı ayrı çalıştırmak için bir güç bankası (5V, 3A olarak derecelendirilmiş) kullandım. Raspberry pi'yi 1 kaynaktan beslemek için bir düşürücü dönüştürücü (buck dönüştürücü: 3A çıkış akımı) kullanılmalıdır.
- 3s(12 V) LiPo pil: Lityum Polimer piller, robotik alanındaki mükemmel performanslarıyla bilinir. Motor sürücüsüne güç sağlamak için kullanılır. Benimkini buradan aldım.
- Erkekten erkeğe ve dişiden dişiye atlama telleri.
- Çift taraflı bant: Bileşenleri RC arabaya monte etmek için kullanılır.
- Mavi bant: Bu projenin çok önemli bir bileşenidir, arabanın arasında gideceği iki şerit çizgisini yapmak için kullanılır. Dilediğiniz rengi seçebilirsiniz ama çevrenizdekilerden farklı renkler seçmenizi tavsiye ederim.
- Zip bağları ve ahşap çubuklar.
- Tornavida.
2. Adım: Raspberry Pi'ye OpenCV Kurulumu ve Uzak Ekranı Ayarlama
Bu adım biraz can sıkıcıdır ve biraz zaman alacaktır.
OpenCV (Açık Kaynaklı Bilgisayarla Görme), açık kaynaklı bir bilgisayarla görme ve makine öğrenimi yazılım kütüphanesidir. Kütüphane 2500'den fazla optimize edilmiş algoritmaya sahiptir. Ahududu pi'nize openCV'yi kurmak ve ahududu pi işletim sistemini kurmak için (hala yapmadıysanız) BU çok basit kılavuzu izleyin. Lütfen unutmayın, openCV'yi oluşturma süreci iyi soğutulmuş bir odada yaklaşık 1,5 saat sürebilir (çünkü işlemcinin sıcaklığı çok yükselecektir!) bu yüzden biraz çay için ve sabırla bekleyin:D.
Uzak ekran için, Windows/Mac cihazınızdan ahududu pi'nize uzaktan erişim kurmak için BU kılavuzu da takip edin.
Adım 3: Parçaları Birbirine Bağlama
Yukarıdaki resimler ahududu pi, kamera modülü ve motor sürücüsü arasındaki bağlantıları göstermektedir. Kullandığım motorların her biri 9 V'ta 0,35 A emdiğini ve bu nedenle motor sürücüsünün aynı anda 3 motoru çalıştırmasını güvenli hale getirdiğini lütfen unutmayın. Ve 2 adet kısma motorunun hızını (1 arka ve 1 ön) tamamen aynı şekilde kontrol etmek istediğim için onları aynı porta bağladım. Motor sürücüsünü çift bant kullanarak arabanın sağ tarafına monte ettim. Kamera modülüne gelince, yukarıdaki resimde gösterildiği gibi vida delikleri arasına bir fermuar bağladım. Ardından kameranın konumunu istediğim gibi ayarlayabilmek için kamerayı ahşap bir çubuğa yerleştiriyorum. Kamerayı mümkün olduğunca arabanın ortasına yerleştirmeye çalışın. Aracın önündeki görüş alanının daha iyi olması için kamerayı yerden en az 20 cm yukarı yerleştirmenizi öneririm. Fritzing şeması aşağıda eklenmiştir.
Adım 4: İlk Test
Kamera Testi:
Kamera kurulduktan ve openCV kütüphanesi oluşturulduktan sonra, ilk imajımızı test etme zamanı! Pi cam'dan bir fotoğraf çekip "orijinal.jpg" olarak kaydedeceğiz. 2 şekilde yapılabilir:
1. Terminal Komutlarını Kullanma:
Yeni bir terminal penceresi açın ve aşağıdaki komutu yazın:
raspistill -o orijinal.jpg
Bu hareketsiz bir görüntü alacak ve onu "/pi/original.jpg" dizinine kaydedecektir.
2. Herhangi bir python IDE kullanarak (IDLE kullanıyorum):
Yeni bir çizim açın ve aşağıdaki kodu yazın:
cv2'yi içe aktar
video = cv2. VideoCapture(0) iken True: ret, çerçeve = video.read() çerçeve = cv2.flip(frame, -1) # görüntüyü dikey olarak çevirmek için kullanılır cv2.imshow('orijinal', çerçeve) cv2. imwrite('orijinal.jpg', çerçeve) anahtarı = cv2.waitKey(1) eğer anahtar == 27: break video.release() cv2.destroyAllWindows()
Bakalım bu kodda neler olmuş. İlk satır, tüm işlevlerini kullanmak için openCV kitaplığımızı içe aktarmaktır. VideoCapture(0) işlevi, bu işlev tarafından belirlenen kaynaktan canlı bir video akışını başlatır, bu durumda, raspi kamera anlamına gelen 0'dır. birden fazla kameranız varsa, farklı numaralar yerleştirilmelidir. video.read(), kameradan gelen her kareyi okuyacak ve "frame" adlı bir değişkene kaydedecektir. flip() işlevi, kameramı ters monte ettiğim için görüntüyü y eksenine göre (dikey olarak) çevirir. imshow(), "orijinal" kelimesi ile başlayan çerçevelerimizi gösterecek ve imwrite(), fotoğrafımızı orijinal-j.webp
OpenCV işlevlerine aşina olmak için fotoğrafınızı ikinci yöntemle test etmenizi öneririm. Görüntü "/pi/original.jpg" dizinine kaydedilir. Fotoğraf makinemin çektiği orijinal fotoğraf yukarıda gösterilmektedir.
Test Motorları:
Bu adım, her motorun dönüş yönünü belirlemek için gereklidir. İlk olarak, bir motor sürücüsünün çalışma prensibi hakkında kısa bir giriş yapalım. Yukarıdaki resim motor sürücüsünün pin çıkışını göstermektedir. Etkinleştirme A, Giriş 1 ve Giriş 2, motor A kontrolü ile ilişkilidir. Etkinleştirme B, Giriş 3 ve Giriş 4, motor B kontrolü ile ilişkilidir. Yön kontrolü "Giriş" kısmı ile hız kontrolü ise "Etkinleştir" kısmı ile sağlanmaktadır. Örneğin A motorunun yönünü kontrol etmek için Giriş 1'i YÜKSEK olarak ayarlayın (bu durumda ahududu pi kullandığımız için 3,3 V) ve Giriş 2'yi DÜŞÜK olarak ayarlayın, motor belirli bir yönde dönecek ve zıt değerleri ayarlayarak Giriş 1 ve Giriş 2'ye göre motor ters yönde dönecektir. Giriş 1 = Giriş 2 = (YÜKSEK veya DÜŞÜK) ise motor dönmez. Etkinleştirme pimleri, ahudududan (0 ila 3,3 V) bir Darbe Genişliği Modülasyonu (PWM) giriş sinyali alır ve motorları buna göre çalıştırır. Örneğin %100 PWM sinyali maksimum hızda çalıştığımız anlamına gelir ve %0 PWM sinyali motorun dönmediği anlamına gelir. Motorların yönlerini belirlemek ve hızlarını test etmek için aşağıdaki kod kullanılır.
ithalat zamanı
RPi. GPIO'yu GPIO olarak içe aktar GPIO.setwarnings(False) # Direksiyon Motoru Pinleri direksiyon_enable = 22 # Fiziksel Pin 15 in1 = 17 # Fiziksel Pin 11 in2 = 27 # Fiziksel Pin 13 #Gaz Motoru Pinleri throttle_enable = 25 # Fiziksel Pin 22 in3 = 23 # Fiziksel Pin 16 in4 = 24 # Fiziksel Pin 18 GPIO.setmode(GPIO. BCM) # GPIO.setup(in1, GPIO.out) GPIO.setup(in2, GPIO.out) GPIO fiziksel numaralandırma yerine GPIO numaralandırmasını kullanın. setup(in3, GPIO.out) GPIO.setup(in4, GPIO.out) GPIO.setup(throttle_enable, GPIO.out) GPIO.setup(steering_enable, GPIO.out) # Direksiyon Motoru Kontrolü GPIO.output(in1, GPIO. YÜKSEK) GPIO.output(in2, GPIO. LOW) direksiyon = GPIO. PWM(steering_enable, 1000) # anahtarlama frekansını 1000 Hz olarak ayarlayın direksiyon.stop() # Gaz Kelebeği Motorları Kontrolü GPIO.output(in3, GPIO. HIGH) GPIO.output(in4, GPIO. LOW) throttle = GPIO. PWM(throttle_enable, 1000) # anahtarlama frekansını 1000 Hz olarak ayarlayın throttle.stop() time.sleep(1) throttle.start(25) # motoru 25'te çalıştırır % PWM sinyali-> (0.25 * akü Voltajı) - sürücünün direksiyon kaybı.start(100) # motoru %100 PWM sinyalinde çalıştırır-> (1 * Akü Voltajı) - sürücünün kayıp süresi.uyku(3) throttle.stop() direksiyon.stop()
Bu kod, kısma motorlarını ve direksiyon motorunu 3 saniye çalıştıracak ve ardından durduracaktır. (Sürücü kaybı) bir voltmetre kullanılarak belirlenebilir. Örneğin, %100 PWM sinyalinin motorun terminalinde tam pilin voltajını vermesi gerektiğini biliyoruz. Ancak, PWM'yi %100'e ayarlayarak, sürücünün 3 V'luk bir düşüşe neden olduğunu ve motorun 12 V yerine 9 V aldığını buldum (tam olarak ihtiyacım olan şey!). Kayıp doğrusal değildir, yani %100'deki kayıp, %25'teki kayıptan çok farklıdır. Yukarıdaki kodu çalıştırdıktan sonra sonuçlarım aşağıdaki gibiydi:
Kısma Sonuçları: in3 = YÜKSEK ve in4 = DÜŞÜK ise, kısma motorları Saat Yönünde (CW) bir dönüşe sahip olacak, yani araba ileri hareket edecektir. Aksi takdirde, araba geriye doğru hareket edecektir.
Direksiyon Sonuçları: eğer in1 = YÜKSEK ve in2 = DÜŞÜK ise, direksiyon motoru maksimum sola dönecektir, yani araba sola dönecektir. Aksi takdirde, araba sağa dönecektir. Bazı deneylerden sonra, PWM sinyali %100 değilse direksiyon motorunun dönmediğini buldum (yani motor ya tamamen sağa ya da tamamen sola dönecek).
Adım 5: Şerit Çizgilerini Tespit Etme ve Yön Çizgisini Hesaplama
Bu adımda arabanın hareketini kontrol edecek algoritma anlatılacaktır. İlk resim tüm süreci gösterir. Sistemin girdisi görüntüler, çıktısı tetadır (derece cinsinden direksiyon açısı). İşlemin 1 görüntü üzerinde yapıldığını ve tüm karelerde tekrarlanacağını unutmayın.
Kamera:
Kamera (320 x 240) çözünürlükte bir video kaydetmeye başlayacaktır. Her kareye işleme teknikleri uyguladıktan sonra fps düşüşü olacağından daha iyi kare hızı (fps) elde edebilmeniz için çözünürlüğü düşürmenizi öneririm. Aşağıdaki kod programın ana döngüsü olacak ve her adımı bu kodun üzerine ekleyecektir.
cv2'yi içe aktar
numpy'yi np video olarak içe aktar = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) # genişliği 320 p olarak ayarla video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) # yüksekliği 240 p olarak ayarla # Döngü while True: ret, frame = video.read() frame = cv2.flip(frame, -1) cv2.imshow("orijinal", frame) key = cv2.waitKey(1) if key == 27: videoyu kes. () cv2.destroyAllWindows()
Buradaki kod, 4. adımda elde edilen orijinal görüntüyü gösterecek ve yukarıdaki resimlerde gösterilmektedir.
HSV Renk Alanına Dönüştür:
Şimdi, kameradan kareler olarak video kaydı aldıktan sonraki adım, her kareyi Ton, Doygunluk ve Değer (HSV) renk uzayına dönüştürmektir. Bunu yapmanın ana avantajı, renkleri parlaklık seviyelerine göre ayırt edebilmektir. Ve işte HSV renk uzayının iyi bir açıklaması. HSV'ye dönüştürme, aşağıdaki işlev aracılığıyla yapılır:
def convert_to_HSV(çerçeve):
hsv = cv2.cvtColor(çerçeve, cv2. COLOR_BGR2HSV) cv2.imshow("HSV", hsv) dönüş hsv
Bu işlev ana döngüden çağrılacak ve çerçeveyi HSV renk uzayında döndürecektir. HSV renk uzayında benim tarafımdan elde edilen çerçeve yukarıda gösterilmiştir.
Mavi Rengi ve Kenarları Algıla:
Görüntüyü HSV renk uzayına dönüştürdükten sonra, yalnızca ilgilendiğimiz rengi (yani şerit çizgilerinin rengi olduğu için mavi rengi) algılamanın zamanı geldi. Bir HSV çerçevesinden mavi rengi çıkarmak için bir ton, doygunluk ve değer aralığı belirtilmelidir. HSV değerleri hakkında daha iyi bir fikir edinmek için buraya bakın. Bazı deneylerden sonra mavi rengin alt ve üst limitleri aşağıdaki kodda gösterilmiştir. Ve her karedeki genel bozulmayı azaltmak için, kenarlar yalnızca keskin kenar detektörü kullanılarak algılanır. Canny Edge hakkında daha fazla bilgiyi burada bulabilirsiniz. Temel bir kural, Canny() işlevinin parametrelerini 1:2 veya 1:3 oranında seçmektir.
def algılama_kenarları(çerçeve):
alt_mavi = np.array([90, 120, 0], dtype = "uint8") # mavi rengin alt limiti üst_blue = np.array([150, 255, 255], dtype="uint8") # üst limiti mavi renk maskesi = cv2.inRange(hsv, alt_mavi, üst_mavi) # bu maske mavi dışındaki her şeyi filtreleyecektir # kenarları algıla kenarları = cv2. Canny(maske, 50, 100) cv2.imshow("kenarlar", kenarlar) dönüş kenarları
Bu fonksiyon, HSV renk uzayı çerçevesini parametre olarak alan ve kenarlı çerçeveyi döndüren ana döngüden de çağrılacaktır. Elde ettiğim kenarlı çerçeve yukarıda bulunuyor.
İlgi Alanı (ROI) seçin:
Çerçevenin yalnızca 1 bölgesine odaklanmak için ilgilenilen bölgenin seçilmesi çok önemlidir. Bu durumda, arabanın ortamda çok fazla eşya görmesini istemiyorum. Ben sadece arabanın şerit çizgilerine odaklanmasını ve başka hiçbir şeyi görmezden gelmesini istiyorum. Not: koordinat sistemi (x ve y eksenleri) sol üst köşeden başlar. Başka bir deyişle, (0, 0) noktası sol üst köşeden başlar. y ekseni yükseklik ve x ekseni genişliktir. Aşağıdaki kod, yalnızca çerçevenin alt yarısına odaklanmak için ilgilenilen bölgeyi seçer.
def bölge_of_interest(kenarlar):
yükseklik, genişlik = kenarlar.şekil # kenarların yüksekliğini ve genişliğini çıkar çerçeve maskesi = np.zeros_like(kenarlar) # kenarlarla aynı boyutlara sahip boş bir matris yap çerçeve # sadece ekranın alt yarısına odaklan # koordinatlarını belirt 4 nokta (sol alt, sol üst, sağ üst, sağ alt) çokgen = np.array(
Bu işlev, kenarlı çerçeveyi parametre olarak alır ve 4 ön ayar noktasına sahip bir çokgen çizer. Yalnızca çokgenin içindekilere odaklanır ve dışındaki her şeyi görmezden gelir. İlgi alanım çerçevesi yukarıda gösterilmiştir.
Çizgi Segmentlerini Algıla:
Hough dönüşümü, kenarlı bir çerçeveden çizgi parçalarını tespit etmek için kullanılır. Hough dönüşümü, herhangi bir şekli matematiksel biçimde algılamak için bir tekniktir. Belirli bir oy sayısına göre bozulmuş olsa bile hemen hemen her nesneyi algılayabilir. Hough dönüşümü için harika bir referans burada gösterilmiştir. Bu uygulama için, her karedeki satırları algılamak için cv2. HoughLinesP() işlevi kullanılır. Bu fonksiyonun aldığı önemli parametreler şunlardır:
cv2. HoughLinesP(çerçeve, rho, teta, min_threshold, minLineLength, maxLineGap)
- Çerçeve: içindeki çizgileri algılamak istediğimiz çerçevedir.
- rho: Piksel cinsinden mesafe hassasiyetidir (genellikle = 1)
- teta: radyan cinsinden açısal kesinlik (her zaman = np.pi/180 ~ 1 derece)
- min_threshold: çizgi olarak kabul edilmesi için alması gereken minimum oy
- minLineLength: piksel cinsinden minimum çizgi uzunluğu. Bu sayıdan daha kısa olan satırlar satır sayılmaz.
- maxLineGap: 1 satır olarak kabul edilecek 2 satır arasındaki piksel cinsinden maksimum boşluk. (Kullandığım şerit çizgilerinde boşluk olmadığı için benim durumumda kullanılmıyor).
Bu işlev, bir satırın uç noktalarını döndürür. Hough dönüşümü kullanılarak satırları algılamak için ana döngümden aşağıdaki işlev çağrılır:
def algılama_hattı_segmentleri(cropped_edges):
rho = 1 teta = np.pi / 180 min_threshold = 10 line_segments = cv2. HoughLinesP(cropped_edges, rho, theta, min_threshold, np.array(), minLineLength=5, maxLineGap=0) line_segments döndürür
Ortalama eğim ve Kesişme (m, b):
doğru denkleminin y = mx + b ile verildiğini hatırlayın. Burada m doğrunun eğimi ve b y-kesişim noktasıdır. Bu kısımda, Hough dönüşümü kullanılarak tespit edilen doğru parçalarının eğimlerinin ve kesişimlerinin ortalaması hesaplanacaktır. Bunu yapmadan önce, yukarıda gösterilen orijinal çerçeve fotoğrafına bir göz atalım. Sol şerit yukarı doğru gidiyor gibi görünüyor, bu nedenle negatif bir eğime sahip (koordinat sistemi başlangıç noktasını hatırlıyor musunuz?). Diğer bir deyişle, sol şerit çizgisi pozitif bir eğim verecek olan x1 < x2 ve y2 x1 ve y2 > y1'e sahiptir. Bu nedenle, pozitif eğimli tüm çizgiler sağ şerit noktaları olarak kabul edilir. Dikey çizgiler durumunda (x1 = x2) eğim sonsuz olacaktır. Bu durumda hata almamak için tüm dikey satırları atlayacağız. Bu algılamaya daha fazla doğruluk eklemek için, her çerçeve 2 sınır çizgisi ile iki bölgeye (sağ ve sol) bölünür. Sağ sınır çizgisinden daha büyük olan tüm genişlik noktaları (x ekseni noktaları), sağ şerit hesaplaması ile ilişkilendirilir. Ve tüm genişlik noktaları sol sınır çizgisinden daha azsa, sol şerit hesaplaması ile ilişkilendirilirler. Aşağıdaki işlev, çerçeveyi işleme altına alır ve Hough dönüşümü kullanılarak algılanan şerit segmentlerini alır ve iki şerit çizgisinin ortalama eğimini ve kesişimini döndürür.
def ortalama_slope_intercept(çerçeve, line_segments):
lane_lines = line_segments None ise: print("hat segmenti algılanmadı") dönüş lane_lines height, width, _ = frame.shape left_fit = right_fit = border = left_region_boundary = genişlik * (1 - sınır) right_region_boundary = genişlik * line_segment için sınır: line_segment içinde x1, y1, x2, y2 için line_segment: if x1 == x2: print("dikey çizgileri atlama (eğim = sonsuz)") devam fit = np.polyfit((x1, x2), (y1, y2), 1) eğim = (y2 - y1) / (x2 - x1) kesişim noktası = y1 - (eğim * x1) eğer eğim < 0 ise: eğer x1 < sol_bölge_sınır ve x2 sağ_bölge_sınır ve x2 > sağ_bölge_sınır: sağa_uygun. 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)) # lane_lines, sağ ve sol şerit çizgilerinin koordinatlarından oluşan 2 boyutlu bir dizidir # örneğin: lan e_lines =
make_points(), şerit çizgilerinin (çerçevenin altından ortasına kadar) sınırlı koordinatlarını döndürecek olan ortalama_slope_intercept() işlevi için bir yardımcı işlevdir.
def make_points(çerçeve, satır):
yükseklik, genişlik, _ = çerçeve.şekil eğim, kesişme = çizgi y1 = yükseklik # çerçevenin altı y2 = int(y1 / 2) # eğer eğim == 0 ise çerçevenin ortasından aşağıya doğru noktalar yapın == 0: eğim = 0.1 x1 = int((y1 - kesişim) / eğim) x2 = int((y2 - kesişim) / eğim) dönüş
0'a bölmeyi önlemek için bir koşul sunulur. y1 = y2 (yatay çizgi) anlamına gelen eğim = 0 ise, eğime 0'a yakın bir değer verin. Bu, algoritmanın performansını etkilemeyeceği gibi imkansız durumu (0'a bölme) de önleyecektir.
Çerçevelerdeki şerit çizgilerini görüntülemek için aşağıdaki fonksiyon kullanılır:
def display_lines(frame, lines, line_color=(0, 255, 0), line_width=6): # line color (B, G, R)
line_image = np.zeros_like(frame) eğer satırlar Yok değilse: satır içi satır için: x1, y1, x2, satırdaki y2 için: cv2.line(line_image, (x1, y1), (x2, y2), line_color, line_width) line_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1) satır_resmi döndür
cv2.addWeighted() işlevi aşağıdaki parametreleri alır ve iki görüntüyü birleştirmek için kullanılır, ancak her birine bir ağırlık verilir.
cv2.addWeighted(image1, alfa, image2, beta, gama)
Ve aşağıdaki denklemi kullanarak çıktı görüntüsünü hesaplar:
çıktı = alfa * görüntü1 + beta * görüntü2 + gama
cv2.addWeighted() işlevi hakkında daha fazla bilgi buradan türetilmiştir.
Başlık Satırını Hesapla ve Göster:
Bu, motorlarımıza hız uygulamadan önceki son adımdır. Yön çizgisi, direksiyon motoruna dönmesi gereken yönü vermekten ve kısma motorlarına çalışacakları hızı vermekten sorumludur. Başlık satırının hesaplanması saf trigonometri olup, tan ve atan (tan^-1) trigonometrik fonksiyonlar kullanılmaktadır. Bazı aşırı durumlar, kameranın yalnızca bir şerit çizgisi algılaması veya herhangi bir çizgi algılamamasıdır. Tüm bu durumlar aşağıdaki fonksiyonda gösterilir:
def get_steering_angle(çerçeve, lane_lines):
yükseklik, genişlik, _ = frame.shape if len(lane_lines) == 2: # eğer iki şerit çizgisi algılanırsa _, _, left_x2, _ = lane_lines[0][0] # lane_lines dizisinden sol x2'yi çıkar _, _, right_x2, _ = lane_lines[1][0] # lane_lines dizisinden sağ x2'yi çıkar mid = int(width / 2) x_offset = (left_x2 + right_x2) / 2 - mid y_offset = int(height / 2) elif len(lane_lines)) == 1: # sadece bir satır algılanırsa x1, _, x2, _ = lane_lines[0][0] x_offset = x2 - x1 y_offset = int(height / 2) elif len(lane_lines) == 0: # herhangi bir çizgi algılanmazsa x_offset = 0 y_offset = int(height / 2) angle_to_mid_radian = mat.atan(x_offset / y_offset) angle_to_mid_deg = int(angle_to_mid_radyan * 180.0 / mat.pi) direksiyon_açı = angle_to_mid_deg + 90 dönüş direksiyon_açı
x_offset ilk durumda ortalamanın ((sağ x2 + sol x2)/2) ekranın ortasından ne kadar farklı olduğudur. y_offset her zaman yükseklik / 2 olarak alınır. Yukarıdaki son resim bir başlık satırı örneğini göstermektedir. angle_to_mid_radians, yukarıdaki son resimde gösterilen "teta" ile aynıdır. Direksiyon_açı = 90 ise, arabanın "yükseklik / 2" hattına dik bir pruva hattına sahip olduğu ve arabanın direksiyonsuz ilerleyeceği anlamına gelir. Direksiyon açısı > 90 ise, araba sağa, aksi takdirde sola dönmelidir. Başlık satırını görüntülemek için aşağıdaki fonksiyon kullanılır:
def display_heading_line(çerçeve, direksiyon_açısı, line_color=(0, 0, 255), line_width=5)
head_image = np.zeros_like(çerçeve) yükseklik, genişlik, _ = frame.shape direksiyon_angle_radian = direksiyon_açısı / 180.0 * mat.pi x1 = int(genişlik / 2) y1 = yükseklik x2 = int(x1 - yükseklik / 2 / mat.tan (steering_angle_radian)) y2 = int(height / 2) cv2.line(heading_image, (x1, y1), (x2, y2), line_color, line_width) header_image = cv2.addWeighted(frame, 0.8, head_image, 1, 1) başlık_resmi döndür
Yukarıdaki fonksiyon, istikamet çizgisinin çizileceği çerçeveyi ve direksiyon açısını girdi olarak alır. Başlık satırının görüntüsünü döndürür. Benim durumumda alınan başlık satırı çerçevesi yukarıdaki resimde gösterilmektedir.
Tüm Kodu Bir Arada Birleştirmek:
Artık kod birleştirilmeye hazırdır. Aşağıdaki kod, her bir işlevi çağıran programın ana döngüsünü gösterir:
cv2'yi içe aktar
numpy'yi np video olarak içe aktar = cv2. VideoCapture(0) video.set(cv2. CAP_PROP_FRAME_WIDTH, 320) video.set(cv2. CAP_PROP_FRAME_HEIGHT, 240) iken True: ret, çerçeve = video.read() çerçeve = cv2.flip(frame, -1) #Fonksiyonları çağırma hsv = convert_to_HSV(frame) edge = tespit_edges(hsv) roi = bölge_of_ilgi(edges) line_segments = tespit_line_segments(roi) lane_lines = ortalama_slope_intercept(frame, line_segments) lane_lines_image, direksiyon_angle_lines(= get_steering_angle(frame, lane_lines) head_image = display_heading_line(lane_lines_image, direksiyon_angle) tuşu = cv2.waitKey(1) if tuşu == 27: videoyu kes.release() cv2.destroyAllWindows()
Adım 6: PD Kontrolünün Uygulanması
Artık motorlara beslenmeye hazır direksiyon açımız var. Daha önce de belirtildiği gibi, direksiyon açısı 90'dan büyükse, araba sağa, aksi takdirde sola dönmelidir. Sabit bir kısma hızında (%10 PWM) açı 90'ın üzerindeyse direksiyon motorunu sağa, direksiyon açısı 90'dan küçükse sola çeviren basit bir kod uyguladım ama çok fazla hata alıyorum. Aldığım en büyük hata, araba herhangi bir dönüşe yaklaştığında direksiyon motorunun doğrudan hareket etmesi ancak kısma motorlarının sıkışması. Kısma hızını dönüşlerde (%20 PWM) olacak şekilde artırmaya çalıştım ama robotun şeritlerden çıkmasıyla sonuçlandı. Direksiyon açısı çok büyükse kısma hızını çok artıran ve direksiyon açısı o kadar büyük değilse hızı biraz artıran, ardından araba 90 dereceye yaklaştıkça (düz hareket ederken) hızı ilk değerine düşüren bir şeye ihtiyacım vardı. Çözüm, bir PD denetleyicisi kullanmaktı.
PID denetleyici, Orantılı, İntegral ve Türev denetleyici anlamına gelir. Bu tip lineer kontrolörler robotik uygulamalarda yaygın olarak kullanılmaktadır. Yukarıdaki resim, tipik PID geri besleme kontrol döngüsünü göstermektedir. Bu kontrolörün amacı, tesisi bazı koşullara göre açıp kapatan "açma-kapama" kontrolörlerinden farklı olarak en verimli şekilde "setpoint"e ulaşmaktır. Bazı anahtar kelimeler bilinmelidir:
- Setpoint: Sisteminizin ulaşmasını istediğiniz istenen değerdir.
- Gerçek değer: Sensör tarafından algılanan gerçek değerdir.
- Hata: ayar noktası ile gerçek değer arasındaki farktır (hata = Ayar noktası - Gerçek değer).
- Kontrollü değişken: adından kontrol etmek istediğiniz değişken.
- Kp: Oransal sabit.
- Ki: İntegral sabiti.
- Kd: Türev sabiti.
Kısaca PID kontrol sistemi döngüsü şu şekilde çalışır:
- Kullanıcı, sistemin ulaşması için gereken ayar noktasını tanımlar.
- Hata hesaplanır (hata = ayar noktası - gerçek).
- P kontrolörü, hatanın değeriyle orantılı bir eylem üretir. (hata artar, P eylemi de artar)
- I denetleyicisi, sistemin sabit durum hatasını ortadan kaldıran ancak aşmasını artıran hatayı zamanla entegre edecektir.
- D denetleyicisi, hatanın zamana göre türevidir. Başka bir deyişle, hatanın eğimidir. Hatanın türeviyle orantılı bir işlem yapar. Bu kontrolör sistemin kararlılığını arttırır.
- Denetleyicinin çıkışı, üç denetleyicinin toplamı olacaktır. Hata 0 olursa kontrolörün çıkışı 0 olur.
PID denetleyicisinin harika bir açıklaması burada bulunabilir.
Şeritte kalma arabasına geri döndüğümde, kontrollü değişkenim hız kesmeydi (direksiyonun sağda veya solda sadece iki durumu olduğu için). Bu amaç için bir PD kontrolörü kullanılır, çünkü D eylemi, hata değişikliği çok büyükse (yani büyük sapma) kısma hızını çok arttırır ve bu hata değişikliği 0'a yaklaşırsa arabayı yavaşlatır. Bir PD uygulamak için aşağıdaki adımları yaptım. denetleyici:
- Ayar noktasını 90 dereceye ayarlayın (arabanın her zaman düz hareket etmesini isterim)
- Ortadan sapma açısı hesaplandı
- Sapma iki bilgi verir: Hatanın ne kadar büyük olduğu (sapmanın büyüklüğü) ve direksiyon motorunun hangi yöne gitmesi gerektiği (sapma işareti). Sapma pozitif ise araç sağa, aksi halde sola dönmelidir.
- Sapma negatif veya pozitif olduğundan, bir "hata" değişkeni tanımlanır ve her zaman sapmanın mutlak değerine eşittir.
- Hata sabit bir Kp ile çarpılır.
- Hata, zaman farklılaşmasına maruz kalır ve sabit bir Kd ile çarpılır.
- Motorların hızı güncellenir ve döngü yeniden başlar.
Kısma motorlarının hızını kontrol etmek için ana döngüde aşağıdaki kod kullanılır:
hız = 10 # % PWM cinsinden çalışma hızı
#Her döngüde güncellenecek değişkenler lastTime = 0 lastError = 0 # PD sabitleri Kp = 0.4 Kd = Kp * 0.65 Doğru iken: şimdi = time.time() # mevcut zaman değişkeni dt = şimdi - lastTime sapma = direksiyon_açı - 90 # eşdeğer açısına_to_mid_deg değişkenine hata = abs(sapma) eğer sapma -5 ise: # 10 derecelik bir hata aralığı varsa yönlendirme yapmayın sapma = 0 hata = 0 GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. LOW) direksiyon.stop() elif sapması > 5: # sapma pozitif ise sağa dön GPIO.output(in1, GPIO. LOW) GPIO.output(in2, GPIO. HIGH) direksiyon.start(100) elif sapması < -5: # sapma negatif ise sola yönlendir GPIO.output(in1, GPIO. HIGH) GPIO.output(in2, GPIO. LOW) direksiyon.start(100) türevi = kd * (hata - lastError) / dt orantılı = kp * hata PD = int(hız + türev + orantılı) spd = abs(PD) eğer spd> 25: spd = 25 throttle.start(spd) lastError = error lastTime = time.time()
Hata çok büyükse (ortadan sapma yüksek), orantısal ve türevsel eylemler yüksektir ve bu da yüksek kısma hızına neden olur. Hata 0'a yaklaştığında (ortadan sapma düşük), türev eylemi tersine hareket eder (eğim negatif) ve sistemin kararlılığını korumak için kısma hızı düşer. Kodun tamamı aşağıda eklenmiştir.
7. Adım: Sonuçlar
Yukarıdaki videolar elde ettiğim sonuçları gösteriyor. Daha fazla ayar ve daha fazla ayarlamaya ihtiyacı var. Ahududu pi'yi LCD ekranıma bağlıyordum çünkü ağım üzerinden video akışı yüksek gecikmeye sahipti ve çalışmak çok sinir bozucuydu, bu yüzden videoda ahududu pi'ye bağlı teller var. Pisti çizmek için köpük tahtalar kullandım.
Bu projeyi daha iyi hale getirmek için önerilerinizi bekliyorum! Umarım bu talimat size yeni bilgiler verecek kadar iyi olmuştur.