一步步製作下棋機器人之 完善XY座標控制

匆匆忙忙,又是一週。
馬上五一,湊了十天假期,想想就開心。
但是假期中是生日,又老了一歲了。很多目標都沒有實現,就馬上要到30了,可怕。
30啊!!
唉,時光勻速又決絕的前行不息,推動了沒有返程票的人生旅程。總想着不斷提升自己,不斷豐富生命的意義,不斷拓寬人生的界限,讓人世這一遭不至於太單調。可是回過頭去看,已經走過的路卻是如此的平庸和枯燥。沒有熱血與衝動,沒有懵懂與曖昧,沒有捨我其誰,沒有誰與爭鋒。
也沒有錢。
那些曾嚮往過的青春熱血,怒髮衝冠,呼朋喚友,學富五車,抱得佳人,亦或是爲夢前行,科學前沿,安得廣廈,高端製造,AI智能,名垂青史等等等等,如今,也還都是嚮往。
也慶幸,還有夢,不至於連人生腐爛的理由都沒有。
但是這個藉口還能用多久呢?
最近搬家,幾大箱子的書,很多都是沒翻過幾頁的。這些都是懶惰的憑證。
所以,懶惰是會積累的。當積累到一定的量,就會量變成爲平凡。如果此時還沒被生活磨滅夢想,磨平棱角,就會堆積出不甘。如果此時還是孤身一人,沒有能夠開解的人或是另一伴,就會累積成痛苦。如果此時再用遊戲等快消品短暫緩解,就會衍生出空虛。此時,早已無力對抗生活。
還有一年,不知道到時候能不能變爲魔法師。
繼續努力吧。

說明

上一次的運動由於時間關係,最終沒有實現理想的效果。本次做了優化,並實現了更加便捷的可視化效果。
經過調試,是因爲在label主類中獲取的座標和子類中獲取的座標不一致導致的。
已定義數值如下:
第一臂長度:R1 = 30cm
第二臂長度:R2 = 20cm
座標原點:(x1,y1)=(0,0),這個也是第一個圓的圓心座標
目標點座標:(x2,y2),這個也是第二個圓的圓心座標
運動結果示意:
image

內容分解

目標是鼠標點擊label的位置,獲取對應成機械臂長度的座標位置,然後換算爲點擊運動角度,發送給 【Coppeliasim】來控制機械臂運動到指定位置,實現鼠標點擊運動。
所以具體內容需要實現:

  • 1:鼠標在label中試試獲取座標
  • 2:比例換算,換算爲預定的機械臂座標
  • 3:點擊後獲取點擊座標,並在label中直接顯示輔助示意圖
  • 4:根據座標,換算兩個電機的運動角度
  • 5:將角度發送給 【Coppeliasim】

1:獲取座標、比例換算

  • 具體實現方式在 【ArmPositionControlTest.py】實現。

首先是重寫一個label的實現方式爲 【 class MyLabel(QLabel) 】,傳遞一個父類label用於繼承,用於將label子類綁定到主界面中的label中,並使用【 setMouseTracking(True) 】方法設置鼠標追蹤,實現鼠標位置的實時追蹤,這個方法開啓後,才能在 MyLabel 子類中實現移動鼠標就能主動觸發 【mouseMoveEvent()】 (鼠標移動)事件。在【mouseMoveEvent()】事件中實時獲取鼠標座標,顯示在一個小的label中並實時移動這個label就實現了座標值跟隨鼠標指針運動的效果。裏面同步實現了比例換算。圖片是【660X440】的比例,原點座標在(330,110)處,則左右、下面各是330的寬度,對應機械臂最長50CM,可以正好換算過去。後續爲了方便計算,多數部分的取值都是保留小數點後四位。
實際上,可以在子類中加上點擊移動角度換算和發送,就能實現鼠標移動,機械臂也跟着鼠標實時運動。但這與我想要的不符,就沒這麼寫。
MyLabel 子類如下

class MyLabel(QLabel):
    '''
    重寫label,用於獲取位置調用
    '''
    def __init__(self, parent=QLabel):
        super().__init__(parent)
        self.setMouseTracking(True)  # 開啓鼠標追蹤功能
        self.setGeometry(0,0,660,440)#設置大小,設置成和輸入label一致的大小,覆蓋掉原來的label,可以更改爲傳遞大小的
        

    def mouseMoveEvent(self, e):
        '''
        鼠標移動觸發的回調事件
        名字是固定的
        '''
        x = e.localPos().x() #獲取控件內的相對座標。注意與父類label中的點擊事件獲取的座標的區分
        y = e.localPos().y()
        
        MoveX,MoveY =round((x-330)/660*100,2), round((y-110)/330*50,2)
        if math.sqrt(MoveX*MoveX+MoveY*MoveY) >50.0:
            text = "Out of ArmRange"
        elif math.sqrt(MoveX*MoveX+MoveY*MoveY) <10.0:
            text = "Out of ArmRange"
        else:
            text = 'x: {0}, y: {1}'.format(round((x-330)/660*100,2), round((y-110)/330*50,2))

        MainWindow.MouseMoveLabel.setText(text)   
        MainWindow.MouseMoveLabel.adjustSize() # 自適應寬度
        MainWindow.MouseMoveLabel.move((int)(e.localPos().x()),(int)(e.localPos().y()))  ##移動用於顯示的label到鼠標位置,實現跟隨顯示
        MainWindow.InforShowLable.setText(text)#左下角的label也顯示一下

2:獲取點擊座標,顯示示意圖

在窗口的主類中,調用鼠標釋放事件【mouseReleaseEvent()】,來處理點擊事件(實際測試鼠標單擊事件【mouseClickEvent()】沒有反應,雙擊事件倒是沒問題。因爲鼠標單擊釋放事件效果和單擊事件一樣,所以不多做探究)。
在鼠標釋放事件中,同樣實現鼠標座標獲取和比例換算。要注意,此處獲取的是相對於窗體的座標,而不是相對於label的座標,所以需要獲取並減去label的左上角的座標纔是鼠標相對於label的座標,這也是上一次怎麼計算角度都不對的主要原因。
換算完後,就可以進行角度換算了。換算部分放到後面。
換算完成後,我們會得到以下幾個值:兩臂所在兩端點圓的兩個交點座標、兩交點與原點連線形成的夾角的角度、鼠標點擊點與原點連線所在直線與X軸夾角的角度等值。我們可以用這些值在label中繪製輔助圖形。
因爲一開始,我們就用 QPixmap 相關的函數實現了背景圖顯示:

self.png = QPixmap('ArmRange.png') #畫布 背景圖 畫筆等
self.pngcopy = self.png.copy() #複製一份背景圖用於覆蓋,因爲畫筆畫完後會保留,所以覆蓋原圖實現畫筆清空
self.label_GetPos.setPixmap(self.pngcopy) #設置背景圖    

此處我們就用此作爲畫布實現輔助曲線的繪製:

 self.painter = QPainter(self.pngcopy)

綁定畫筆後,就可以使用畫筆繪圖了。我們實現以下幾個輔助圖的繪製:

  • 1:兩臂所在的圓的繪製
  • 2:經過兩個交點的機械臂的示意圖
  • 3:兩交點的連線示意圖
  • 4:末端點與原點連線的示意圖
  • 5:各個點的座標顯示

(注意,由於背景圖的原圖沒保存,只有上一次由於時間關係手繪的不規則範圍圖,所以畫的示意圖還有上次的不規則圖,懶得再去製作乾淨的背景圖了,可以自行更換乾淨的背景圖)
相關的繪製都在鼠標釋放事件【mouseReleaseEvent】中
顯示後的圖如下:
image

3:交點和角度的計算

首先,我們將機械臂的兩個臂視爲以各自末端端點爲圓心的圓的某一個半徑直線,它們的交點就是這兩個圓的交點,也是我們想要求的座標。

求圓的交點的座標的方法有很多,此處給出兩種較簡單的實現方法,分別是【 CalPointOfSection1 】和【CalPointOfSection2 】,都位於MyArmTest.py中。
方法一是三角函數法,具體參考了【 求解兩圓相交的交點座標 】,使用了裏面的方法一。
另一種方法是以前做機械臂時使用的一種方法。以前還實現過其他算法,比如參考【 已知圓心座標和半徑的兩個圓 的交點座標】,不過這個計算量較多,此處不放了,可自行參考實現。

得到交點座標後,就可以開始求對應座標下的移動角度了。此處由於時間關係,我們只求相對靠左一側的機械臂的運動角度,靠右一側實現方法類似,只是有一些小區別。

示例圖示:

求角度的方法也有很多,此處我們選擇較爲穩妥的方法。

  • 1:先求出鼠標點擊的目標點與原點連線與Y軸的夾角的角度,即圖示 ∠ZOY,使用 【CalAngleOfPoint1】函數
  • 2:求出兩圓交點分別與原點連線所在直線的夾角角度,這個角度被上面的直線平分爲兩半,即圖示 ∠aOb,使用【CalAngleBetweenTwoPoint】函數
  • 3:根據這兩個角就能求出臂一的運動角度,∠aOY = ∠ZOY - ∠aOb/2

開始求臂二的角度:
使用函數【CalAngleOfArm2】

  • 1:上一步已求出∠aOZ,又已知臂一 L(Oa) 的長度,利用三角函數可得 L(an)的長度(也可以用L(ab)的距離除以2得到),同理,可以得到L(On)的長度,已知Z的座標,得到L(OZ)的長度,減去L(On)得到L(Zn)的長度。注意,L(an)或L(Zn)只要求出其中一個就夠了
  • 2:已知L(aZ)的長度,加上上面求出的一個邊,可以得到∠naZ
  • 3:∠Oan = 90-∠ZOa,這樣得到了∠OAZ= ∠Oan+∠naZ,就求出了臂二在a點的旋轉角度
  • 4:注意,臂二默認的角度是垂直於臂一的,所以要用90度減去上面計算的值纔是【Coppeliasim】中實際旋轉的角度。可以自行在【Coppeliasim】軟件中修改角度的默認值。
    以上具體代碼詳見【MyArmTest.py】和 【ArmPositionControlTest.py】

4:發送給Coppeliasim

至此,完成了示意圖的顯示、角度的計算,將計算得到的角度發送給【Coppeliasim】軟件進行運動就好了。

此處由於時間關係,發送的還是最終的計算結果,沒對過程進行插值處理,導致機械臂立刻就轉到目標點了,沒有轉動過程。
實際上,有幾種方法實現轉動過程。

  • 方法一:就是將計算後的角度按照想要的細分度,分爲若干份,然後開啓一個定時器,依次發送細分的角度值,實現平滑運動。定時器的間隔和細分的數量決定了運動速度和運動平滑度,可以使用按鈕設置參數實現調速。
  • 方法二:將目標點與當前點之間按照直線關係或是更高級的曲線關係連接起來,並將連線細分爲若干份,開啓一個定時器,讓末端依次移動到細分位置處實現平滑運動。定時器的間隔和細分的數量決定了運動速度和運動平滑度,可以使用按鈕設置參數實現調速。

這兩種方法各有優缺點。方法一由於只專注於運動角度,所以末端的位置運動不規則,末端運動速度也很難把控,可能在不同段速度都不同。好處是可以實現絕大多數位置的過度。 方法二是末端軌跡明確,末端速度可控,但是對於某些位置的移動,單純的直線實現不了時(比如直線中經過超範圍區,或者需要運動到兩個臂的夾角翻轉的位置),改爲曲線運動會很複雜。
由於時間關係,本次不做插值的實現。

總結

本次實現了點擊後直接顯示結果示意圖、修正了角度計算結果,最終實現了既定的目標。

但是有些必要的功能還是沒有實現的,比如插值運動,比如兩個圓的交點目前只是簡單的使用了其中一個,另一個沒有使用。實際情況中,兩個交點會出現交替使用才能滿足運動結果的情況,此處由於時間關係沒有實現。

另外,本次結果還是有些瑕疵的,比如機械臂無法實現伸直的情況(交點接近或是直接就是一個),如圖:

當然,若是時間充足,解決上述小瑕疵還是沒問題的,只是需要進一步調試。此處不再深入研究。

後續會開啓硬件方面的探究,最終需要用到時再根據實際情況實現和優化必要的功能。

另外,使用矩陣進行座標計算也沒有時間實現。後續使用中會根據情況更換計算方法。

以上代碼位於:【執念執戰Gitee

  • 本文水平有限,內容很多詞語由於知識水平問題不嚴謹或很離譜,但主要作爲記錄作用,希望以後的自己和路過的大神對必要的錯誤提出批評與指點,對可笑的錯誤請指出來,我會改正的。

  • 另外,轉載使用請註明作者和出處,不要刪除文檔中的關於作者的註釋。

隨夢,隨心,隨願,恆執念,爲夢執戰,執戰蒼天! ------------------執念執戰

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章