NAO机器人高尔夫中的视觉系统设计
去年(2017)年分別參加了江蘇省和全國(guó)的NAO機(jī)器人高爾夫比賽,負(fù)責(zé)的是視覺(jué)部分編程。在這里把之前的工作總結(jié)一下。內(nèi)容主要包括紅球和黃桿的識(shí)別和定位(包括在比賽中遇到的一些問(wèn)題和解決辦法)。完整的代碼(C++和Python兩個(gè)版本)見(jiàn)https://github.com/ZhouJiaHuan/nao-golf-visual-task,本篇只以Python代碼為例進(jìn)行介紹。
基本配置(基類(lèi))
在代碼實(shí)現(xiàn)上,為了方便擴(kuò)展,我先定義了一個(gè)基類(lèi)用來(lái)定義一些最基本的配置信息,然后再派生出視覺(jué)類(lèi)(寫(xiě)運(yùn)動(dòng)代碼也可以派生出一個(gè)運(yùn)動(dòng)的類(lèi))、用于紅球黃和黃桿的檢測(cè)?;?lèi)的定義如下:
# date: 1/15/2017 # description: basic class for all Nao tasks. ## ---------------------------------------------------------------------import sys # sys.path.append("/home/meringue/Softwares/pynaoqi-sdk/") # naoqi directory from naoqi import ALProxyclass ConfigureNao(object):"""a basic class for all nao tasks, including motion, bisualization etc."""def __init__(self, IP):self._IP = IPself._PORT = 9559self._cameraProxy = ALProxy("ALVideoDevice", self._IP, self._PORT)self._motionProxy = ALProxy("ALMotion", self._IP, self._PORT)self._postureProxy = ALProxy("ALRobotPosture", self._IP, self._PORT)self._tts = ALProxy("ALTextToSpeech",self._IP, self._PORT)self._memoryProxy = ALProxy("ALMemory", self._IP, self._PORT)從代碼中可以看出,ConfigureNao這個(gè)基類(lèi)中主要包含了一些基本配置信息,如IP,端口號(hào),并且創(chuàng)建了一些需要用到的NAO庫(kù)中自帶的類(lèi)的對(duì)象(視覺(jué)、語(yǔ)音、運(yùn)動(dòng)等)。在等會(huì)我們定義視覺(jué)類(lèi)的時(shí)候,可以直接從ConfigureNao這個(gè)類(lèi)繼承。
視覺(jué)模塊
視覺(jué)基類(lèi)——視覺(jué)任務(wù)的基類(lèi)
由于視覺(jué)任務(wù)中(紅球檢測(cè)、黃桿檢測(cè))都需要共用一些基本功能(如從攝像頭獲取數(shù)據(jù)),因此再定義一個(gè)視覺(jué)基類(lèi)VisualBasis供使用,這個(gè)類(lèi)是從ConfigureNao繼承出來(lái)的。定義如下:
class VisualBasis(ConfigureNao):"""a basic class for visual task."""def __init__(self, IP, cameraId, resolution=vd.kVGA):"""initilization. Args:IP: NAO's IPcameraId: bottom camera (1,default) or top camera (0).resolution: kVGA, default: 640*480)Return: none""" super(VisualBasis, self).__init__(IP)self._cameraId = cameraIdself._resolution = resolutionself._colorSpace = vd.kBGRColorSpaceself._fps = 20self._frameHeight = 0self._frameWidth = 0self._frameChannels = 0self._frameArray = Noneself._cameraPitchRange = 47.64/180*np.piself._cameraYawRange = 60.97/180*np.piself._cameraProxy.setActiveCamera(self._cameraId)def updateFrame(self, client="python_client"):"""get a new image from the specified camera and save it in self._frame.Args:client: client name.Return: none.""""""if self._cameraProxy.getActiveCamera() == self._cameraId:print("current camera has been actived.")else:self._cameraProxy.setActiveCamera(self._cameraId)"""self._videoClient = self._cameraProxy.subscribe(client, self._resolution, self._colorSpace, self._fps)frame = self._cameraProxy.getImageRemote(self._videoClient)self._cameraProxy.unsubscribe(self._videoClient)try:self._frameWidth = frame[0]self._frameHeight = frame[1]self._frameChannels = frame[2]self._frameArray = np.frombuffer(frame[6], dtype=np.uint8).reshape([frame[1],frame[0],frame[2]])except IndexError:raisedef getFrameArray(self):"""get current frame.Return: current frame array (numpy array)."""if self._frameArray is None:return np.array([])return self._frameArraydef showFrame(self):"""show current frame image."""if self._frameArray is None:print("please get an image from Nao with the method updateFrame()")else:cv2.imshow("current frame", self._frameArray)def printFrameData(self):"""print current frame data."""print("frame height = ", self._frameHeight)print("frame width = ", self._frameWidth)print("frame channels = ", self._frameChannels)print("frame shape = ", self._frameArray.shape)def saveFrame(self, framePath):"""save current frame to specified direction. Arguments:framePath: image path."""cv2.imwrite(framePath, self._frameArray)print("current frame image has been saved in", framePath)def setParam(self, paramName=None, paramValue = None):raise NotImplementedErrordef setAllParamsToDefault(self):raise NotImplementedError視覺(jué)基類(lèi)中除了定義了一些默認(rèn)的攝像頭參數(shù),還定義了一些基本的成員函數(shù)、包括從指定攝像頭獲取一幀圖像、返回當(dāng)前存儲(chǔ)的圖像數(shù)據(jù)、顯示當(dāng)前圖像、保存當(dāng)前圖像到本地,還有一些以后用到再定義的函數(shù),先預(yù)留借口在這里。因?yàn)檫@個(gè)類(lèi)中都是一些簡(jiǎn)單的功能,此處不多介紹。
有了上面定義的視覺(jué)類(lèi),就可以繼續(xù)派生出紅球檢測(cè)類(lèi)和黃桿檢測(cè)類(lèi),下面分別介紹。
紅球檢測(cè)類(lèi)
其實(shí)NAO的官方庫(kù)里面提供了紅球識(shí)別的API,但我們測(cè)試過(guò),發(fā)現(xiàn)效果很不好,非常容易受到一些干擾物的影響。因此我們打算基于OpenCV自己寫(xiě)紅球識(shí)別的代碼。最簡(jiǎn)單的思路就是顏色閾值分割+霍夫圓檢測(cè),然而在測(cè)試的時(shí)候我們發(fā)現(xiàn)僅僅通過(guò)這兩個(gè)步驟檢測(cè)的結(jié)果并不穩(wěn)定,于是我們?cè)谶@基礎(chǔ)上針對(duì)比賽環(huán)境做了改進(jìn)。
紅球檢測(cè)類(lèi)的類(lèi)名叫BallDetect,是從VisualBasis類(lèi)繼承出來(lái)的,因此已經(jīng)包含了VisualBasis類(lèi)中的基本屬性和成員函數(shù),我們只要在此基礎(chǔ)上繼續(xù)編寫(xiě)紅球檢測(cè)需要的成員函數(shù)即可。
紅球基本屬性
我們需要保存的紅球相關(guān)的信息有兩塊:(1)紅球在圖像中的位置信息;(2)紅球相對(duì)于機(jī)器人坐標(biāo)系的位置信息。因此我們先定義這兩個(gè)屬性,如下:
def __init__(self, IP, cameraId=vd.kBottomCamera, resolution=vd.kVGA):"""initialization."""super(BallDetect, self).__init__(IP, cameraId, resolution)self._ballData = {"centerX":0, "centerY":0, "radius":0}self._ballPosition= {"disX":0, "disY":0, "angle":0}self._ballRadius = 0.05紅球在圖像中的位置信息和實(shí)際位置信息分別用”_ballData”和”_ballPosition”來(lái)存放,初始值都設(shè)為0
圖像預(yù)處理
該函數(shù)功能是對(duì)圖像進(jìn)行特定通道的分離和濾波,這里我分別針對(duì)RGB空間和HSV空間都寫(xiě)了預(yù)處理函數(shù)供調(diào)用.
RGB空間預(yù)處理函數(shù)
def _getChannelAndBlur(self, color):"""get the specified channel and blur the result.Arguments:color: the color channel to split, only supports the color of red, geen and blue. Return: the specified color channel or None (when the color is not supported)."""try:channelB = self._frameArray[:,:,0]channelG = self._frameArray[:,:,1]channelR = self._frameArray[:,:,2]except:raise Exception("no image detected!")Hm = 6if color == "red":channelB = channelB*0.1*HmchannelG = channelG*0.1*HmchannelR = channelR - channelB - channelGchannelR = 3*channelRchannelR = cv2.GaussianBlur(channelR, (9,9), 1.5)channelR[channelR<0] = 0channelR[channelR>255] = 255return np.uint8(np.round(channelR))elif color == "blue":channelR = channelR*0.1*HmchannelG = channelG*0.1*HmchannelB = channelB - channelG - channelRchannelB = 3*channelB channelB = cv2.GaussianBlur(channelB, (9,9), 1.5)channelB[channelB<0] = 0channelB[channelB>255] = 255return np.uint8(np.round(channelB))elif color == "green":channelB = channelB*0.1*HmchannelR= channelR*0.1*HmchannelG = channelG - channelB - channelRchannelG = 3*channelGchannelG = cv2.GaussianBlur(channelG, (9,9), 1.5)channelG[channelG<0] = 0channelG[channelG>255] = 255return np.uint8(np.round(channelG))else:print("can not recognize the color!")print("supported color:red, green and blue.")return None雖然說(shuō)是紅球檢測(cè),但為了考慮代碼的一般性,我也增加了綠色和藍(lán)色。在分離的時(shí)候增強(qiáng)了紅色通道的值并削減了其他空間的結(jié)果,這樣可以使分離的結(jié)果更好。另外增加高斯濾波讓局部信息模糊有利于霍夫圓的檢測(cè)。
HSV空間預(yù)處理函數(shù)
def _binImageHSV(self, color):"""get binary image from the HSV image (transformed from BGR image)Args:color: the color for binarization.Return:binImage: binary image."""try:frameArray = self._frameArray.copy()imgHSV = cv2.cvtColor(frameArray, cv2.COLOR_BGR2HSV)except:raise Exception("no image detected!")if color == "red":minHSV1=np.array([0,43,46])maxHSV1=np.array([10,255,255])minHSV2=np.array([156,43,46])maxHSV2=np.array([180,255,255])frameBin1 = cv2.inRange(imgHSV, minHSV1, maxHSV1)frameBin2 = cv2.inRange(imgHSV, minHSV2, maxHSV2)frameBin = np.maximum(frameBin1, frameBin2)return frameBinelse:raise Exception("not recognize the color!")這個(gè)函數(shù)的輸入依然是一個(gè)RGB空間圖像,內(nèi)部轉(zhuǎn)換為HSV空間進(jìn)行處理。這里用到了OpenCV庫(kù)中的inRange()函數(shù)進(jìn)行二值化,由于紅色對(duì)應(yīng)的HSV的區(qū)間范圍有兩個(gè),所以在代碼實(shí)現(xiàn)上用了“并”操作。函數(shù)實(shí)現(xiàn)上只提供了紅色的提取,但也可以拓展到其他顏色的預(yù)處理,這里給出一個(gè)各個(gè)顏色HSV空間的實(shí)驗(yàn)取值范圍:
在實(shí)驗(yàn)測(cè)試的時(shí)候,發(fā)現(xiàn)在一般的情況下,兩個(gè)空間都能準(zhǔn)確地把紅球分割出來(lái),但在光線條件較差的時(shí)候(較強(qiáng)或較弱),發(fā)現(xiàn)HSV空間更加穩(wěn)定。
紅球識(shí)別
圖像預(yù)處理后,圖像上會(huì)分割出紅球所在區(qū)域和其他的一些噪聲。理想情況下,紅球所在的區(qū)域分割結(jié)果應(yīng)該是一個(gè)圓(橢圓),這里我是直接通過(guò)OpenCV庫(kù)中的霍夫圓檢測(cè)函數(shù)實(shí)現(xiàn)的:
def _findCircles(self, img, minDist, minRadius, maxRadius):"""detect circles from an image.Arguments:img: image to be detected.minDist: minimum distance between the centers of the detected circles.minRadius: minimum circle radius.maxRadius: maximum circle radius.Return: an uint16 numpy array shaped circleNum*3 if circleNum>0, ([[circleX, circleY,radius]])else return None."""circles = cv2.HoughCircles(np.uint8(img), cv2.HOUGH_GRADIENT, 1, minDist, param1=150, param2=15, minRadius=minRadius, maxRadius=maxRadius)if circles is None:return np.uint16([])else:return np.uint16(np.around(circles[0, ]))根據(jù)比賽用球的大小要求可以大概限制一下紅球在圖像中的半徑范圍(和分辨率有關(guān)),代碼中的參數(shù)是基于640×480的分辨率設(shè)置的。需要注意的是,經(jīng)過(guò)上述霍夫圓檢測(cè)到的球可能有多個(gè)(可能包含了一些噪聲),因此還應(yīng)該對(duì)結(jié)果進(jìn)一步的判斷。
紅球篩選
經(jīng)過(guò)紅球識(shí)別的結(jié)果可能有如下2種情況:
- 圖像中沒(méi)有檢測(cè)到球。
- 圖像中檢測(cè)到一個(gè)或者多個(gè)球。
第一種情況不需要討論,只需要返回沒(méi)有球的信息即可。對(duì)于第二種情況,我們需要對(duì)每一個(gè)檢測(cè)出的紅球進(jìn)行二次判斷。因?yàn)樵诒荣惉F(xiàn)場(chǎng),NAO機(jī)器人最多只應(yīng)該檢測(cè)到一個(gè)球。因此,針對(duì)第二種情況,我給出的篩選方法如下:
對(duì)于每一個(gè)檢測(cè)出的紅球,以紅球圓心為中心,以紅球的4倍半徑為邊長(zhǎng)畫(huà)一個(gè)外圍正方形,計(jì)算外接正方形區(qū)域內(nèi)紅色和綠色像素點(diǎn)所占的比值。一個(gè)簡(jiǎn)單的示意圖如下:
最理想的情況下,紅色像素的比例為πr2/16r2=0.196πr2/16r2=0.196,綠色像素所占的比例為0.8040.804,但在實(shí)際檢測(cè)的時(shí)候,存在各種不確定因素(圓檢測(cè)誤差、光線不均勻?qū)е骂伾畔l(fā)生變化、其他干擾物的影響等),幾乎不可能達(dá)到理想情況。因此,在具體的實(shí)現(xiàn)上,我們需要把條件設(shè)定得寬松點(diǎn)。實(shí)現(xiàn)代碼如下:
def _selectCircle(self, circles):"""select one circle in list type from all circles detected. Args:circles: numpy array shaped (N, 3), N is the number of circles.Return:selected circle or None (no circle is selected)."""if len(circles) == 0 :return circlesif circles.shape[0] == 1:centerX = circles[0][0]centerY = circles[0][1]radius = circles[0][2]initX = centerX - 2*radiusinitY = centerY - 2*radiusif initX<0 or initY<0 or (initX+4*radius)>self._frameWidth or (initY+4*radius)>self._frameHeight or radius<1:return circleschannelB = self._frameArray[:,:,0]channelG = self._frameArray[:,:,1]channelR = self._frameArray[:,:,2]rRatioMin = 1.0; circleSelected = np.uint16([])for circle in circles:centerX = circle[0]centerY = circle[1]radius = circle[2]initX = centerX - 2*radiusinitY = centerY - 2*radiusif initX<0 or initY<0 or (initX+4*radius)>self._frameWidth or (initY+4*radius)>self._frameHeight or radius<1:continuerectBallArea = self._frameArray[initY:initY+4*radius+1, initX:initX+4*radius+1,:]bFlat = np.float16(rectBallArea[:,:,0].flatten())gFlat = np.float16(rectBallArea[:,:,1].flatten())rFlat = np.float16(rectBallArea[:,:,2].flatten())rScore1 = np.uint8(rFlat>1.0*gFlat)rScore2 = np.uint8(rFlat>1.0*bFlat)rScore = float(np.sum(rScore1*rScore2))gScore = float(np.sum(np.uint8(gFlat>1.0*rFlat)))rRatio = rScore/len(rFlat)gRatio = gScore/len(gFlat) print("red ratio = ", rRatio)print("green ratio = ", gRatio)if rRatio>=0.12 and gRatio>=0.1 and abs(rRatio-0.19)<abs(rRatioMin-0.19):circleSelected = circlereturn circleSelected該函數(shù)的輸入是一個(gè)2維數(shù)組,每一個(gè)代表一個(gè)檢測(cè)出來(lái)的紅球信息(x,y,r)(x,y,r),為了保證篩選后最多只剩一個(gè)紅球,代碼最后再所有滿足比例條件中的球中選擇了紅色比例最接近理想值0.19的紅球。
還有一點(diǎn)需要說(shuō)明的是,上面的代碼中使用的是RGB空間進(jìn)行顏色統(tǒng)計(jì)的,在實(shí)現(xiàn)上其實(shí)也可以使用HSV空間進(jìn)行統(tǒng)計(jì),方法還是一樣。一個(gè)完整的紅球檢測(cè)過(guò)程如下:
紅球定位
上面只是把紅球的位置在圖像中定位出來(lái)了,而我們?cè)诒荣愔行枰t球相對(duì)于機(jī)器人(機(jī)器人坐標(biāo)系)的位置信息。比較簡(jiǎn)單的一種定位方法就是三角函數(shù)定位,也就是利用已知的一些參數(shù)(紅球半徑、機(jī)器人攝像頭離地面高度、攝像頭位置、廣角等)構(gòu)造幾個(gè)直角三角形,最后即可得出紅球相當(dāng)于機(jī)器人的位置信息。一個(gè)簡(jiǎn)單的計(jì)算示意圖如下(具體計(jì)算公式見(jiàn)代碼):
對(duì)應(yīng)的計(jì)算位置的代碼如下:
def _updateBallPosition(self, standState):"""compute and update the ball position with the ball data in frame.standState: "standInit" or "standUp"."""bottomCameraDirection = {"standInit":49.2/180*np.pi, "standUp":39.7/180*np.pi} try:cameraDirection = bottomCameraDirection[standState]except KeyError:print("Error! unknown standState, please check the value of stand state!")raiseelse:if self._ballData["radius"] == 0:self._ballPosition= {"disX":0, "disY":0, "angle":0}else:centerX = self._ballData["centerX"]centerY = self._ballData["centerY"]radius = self._ballData["radius"]cameraPos = self._motionProxy.getPosition(self._cameraName, motion.FRAME_WORLD, True)cameraX, cameraY, cameraHeight = cameraPos[:3]head_yaw, head_pitch = self._motionProxy.getAngles("Head", True)camera_pitch = head_pitch + cameraDirectionimg_center_x = self._frameWidth/2img_center_y = self._frameHeight/2center_x = self._ballData["centerX"]center_y = self._ballData["centerY"]img_pitch = (center_y-img_center_y)/(self._frameHeight)*self._cameraPitchRangeimg_yaw = (img_center_x-center_x)/(self._frameWidth)*self._cameraYawRangeball_pitch = camera_pitch + img_pitchball_yaw = img_yaw + head_yawprint("ball yaw = ", ball_yaw/np.pi*180)dis_x = (cameraHeight-self._ballRadius)/np.tan(ball_pitch) + np.sqrt(cameraX**2+cameraY**2)dis_y = dis_x*np.sin(ball_yaw)dis_x = dis_x*np.cos(ball_yaw)self._ballPosition["disX"] = dis_xself._ballPosition["disY"] = dis_yself._ballPosition["angle"] = ball_yaw代碼中前一部分主要是獲取計(jì)算所需要的信息(傳感器的值和常數(shù)項(xiàng)),后一部分是紅球位置計(jì)算公式。最后直接將計(jì)算的結(jié)果保存在類(lèi)中。在實(shí)驗(yàn)的時(shí)候,統(tǒng)計(jì)各個(gè)位置的平均誤差在1-2里面,視野中心的位置誤差較小,視野邊界附近的誤差較大。
這里還需要補(bǔ)充一點(diǎn)的是,之前由于我們代碼中的計(jì)算公式有誤(可對(duì)比上下代碼的不同),導(dǎo)致計(jì)算的位置信息有很大的偏差,具體表現(xiàn)是X方向的距離基本準(zhǔn)確,Y方向的距離信息偏小,而且X越小的時(shí)候相對(duì)偏差越明顯。為此,我們通過(guò)采集視野中的不同位置信息(采集多次數(shù)據(jù)取平均),統(tǒng)計(jì)各個(gè)離散位置的誤差信息,最后用多項(xiàng)式對(duì)誤差進(jìn)行補(bǔ)償。在測(cè)試的時(shí)候發(fā)現(xiàn)補(bǔ)償?shù)慕Y(jié)果可以把誤差縮小到1厘米左右。代碼如下:
def _updateBallPositionFitting(self, standState):"""compute and update the ball position with compensation.Args:standState: "standInit" or "standUp"."""bottomCameraDirection = {"standInit":49.2, "standUp":39.7} ballRadius = self._ballRadiustry:cameraDirection = bottomCameraDirection[standState]except KeyError:print("Error! unknown standState, please check the value of stand state!")raiseelse:if self._ballData["radius"] == 0:self._ballPosition= {"disX":0, "disY":0, "angle":0}else:centerX = self._ballData["centerX"]centerY = self._ballData["centerY"]radius = self._ballData["radius"]cameraPosition = self._motionProxy.getPosition("CameraBottom", 2, True)cameraX = cameraPosition[0]cameraY = cameraPosition[1]cameraHeight = cameraPosition[2]headPitches = self._motionProxy.getAngles("HeadPitch", True)headPitch = headPitches[0]headYaws = self._motionProxy.getAngles("HeadYaw", True)headYaw = headYaws[0]ballPitch = (centerY-240.0)*self._cameraPitchRange/480.0 # y (pitch angle)ballYaw = (320.0-centerX)*self._cameraYawRange/640.0 # x (yaw angle)dPitch = (cameraHeight-ballRadius)/np.tan(cameraDirection/180*np.pi+headPitch+ballPitch)dYaw = dPitch/np.cos(ballYaw)ballX = dYaw*np.cos(ballYaw+headYaw)+cameraXballY = dYaw*np.sin(ballYaw+headYaw)+cameraYballYaw = np.arctan2(ballY, ballX)self._ballPosition["disX"] = ballX# 誤差補(bǔ)償(多項(xiàng)式) if (standState == "standInit"):ky = 42.513*ballX**4 - 109.66*ballX**3 + 104.2*ballX**2 - 44.218*ballX + 8.5526 #ky = 12.604*ballX**4 - 37.962*ballX**3 + 43.163*ballX**2 - 22.688*ballX + 6.0526ballY = ky*ballYballYaw = np.arctan2(ballY,ballX) self._ballPosition["disY"] = ballYself._ballPosition["angle"] = ballYaw最后要實(shí)現(xiàn)上面的所有功能,在類(lèi)中再定義一個(gè)函數(shù),把之前實(shí)現(xiàn)的各個(gè)模塊封裝在一起,如下:
def updateBallData(self, standState="standInit", color="red", color_space="BGR", fitting=False):"""update the ball data with the frame get from the bottom camera.Arguments:standState: ("standInit", default), "standInit" or "standUp".color: ("red", default) the color of ball to be detected.color_space: "BGR", "HSV".fittting: the method of localization.Return: a dict with ball data. for example: {"centerX":0, "centerY":0, "radius":0}."""self.updateFrame()#cv2.imwrite("src_image.jpg", self._frameArray)minDist = int(self._frameHeight/30.0)minRadius = 1maxRadius = int(self._frameHeight/10.0)if color_space == "BGR":grayFrame = self._getChannelAndBlur(color)else:grayFrame = self._binImageHSV(color)#cv2.imshow("bin frame", grayFrame)#cv2.imwrite("bin_frame.jpg", grayFrame)#cv2.waitKey(20)circles = self._findCircles(grayFrame, minDist, minRadius, maxRadius)circle = self._selectCircle(circles)if len(circle) == 0:self._ballData = {"centerX":0, "centerY":0, "radius":0}self._ballPosition= {"disX":0, "disY":0, "angle":0}else: self._ballData = {"centerX":circle[0], "centerY":circle[1], "radius":circle[2]}if fitting == True:self._updateBallPositionFitting(standState=standState)else:self._updateBallPosition(standState=standState)黃桿檢測(cè)類(lèi)
在NAO機(jī)器人高爾夫中,球洞上會(huì)立著一根黃桿用于NAO機(jī)器人遠(yuǎn)程定位球洞的方向。因?yàn)镹AO機(jī)器人的頭部攝像頭看到距離更遠(yuǎn),所以在比賽中會(huì)使用NAO機(jī)器人的頭部攝像頭檢測(cè)黃桿。但這也帶來(lái)一些問(wèn)題,比如會(huì)把遠(yuǎn)處一些黃色的東西錯(cuò)認(rèn)為黃桿。
同樣,我們需要定義一個(gè)黃桿檢測(cè)類(lèi)StickDetect,它也是從視覺(jué)基類(lèi)VisualBasis繼承出來(lái)的。類(lèi)中我定義了三個(gè)屬性:一個(gè)列表用來(lái)存放檢測(cè)出來(lái)的黃桿的位置信息、一個(gè)值用來(lái)存放黃桿相對(duì)于機(jī)器人的角度、一個(gè)常數(shù)用來(lái)確定是否要對(duì)圖像進(jìn)行裁剪。前兩個(gè)很好理解,定義一個(gè)裁剪的常用的原因是在比賽現(xiàn)場(chǎng),當(dāng)NAO機(jī)器人以正常走路的姿勢(shì)去尋找黃桿時(shí),黃桿一般位于圖像的下方,因此將圖像上半圖像剪掉不但可以排除一些干擾物,還可以減少計(jì)算量。具體的定義如下:
def __init__(self, IP, cameraId=vd.kTopCamera, resolution=vd.kVGA):super(StickDetect, self).__init__(IP, cameraId, resolution)self._boundRect = []self._cropKeep = 1self._stickAngle = None # rad在我們的檢測(cè)方法中,采用HSV顏色分割+形狀判別的方式。其中HSV顏色分割和上面用HSV空間進(jìn)行紅球分割是一樣的,只要改一下閾值范圍就行,函數(shù)如下:
def _preprocess(self, minHSV, maxHSV, cropKeep, morphology):"""preprocess the current frame for stick detection.(binalization, crop etc.)Arguments:minHSV: the lower limit for binalization.maxHSV: the upper limit for binalization.cropKeep: crop ratio (>=0.5).morphology: erosion and dilation.Return:preprocessed image for stick detection."""self._cropKeep = cropKeepframeArray = self._frameArrayheight = self._frameHeightwidth = self._frameWidthtry:frameArray = frameArray[int((1-cropKeep)*height):,:]except IndexError:raise frameHSV = cv2.cvtColor(frameArray, cv2.COLOR_BGR2HSV)frameBin = cv2.inRange(frameHSV, minHSV, maxHSV)kernelErosion = np.ones((5,5), np.uint8)kernelDilation = np.ones((5,5), np.uint8) frameBin = cv2.erode(frameBin, kernelErosion, iterations=1)frameBin = cv2.dilate(frameBin, kernelDilation, iterations=1)frameBin = cv2.GaussianBlur(frameBin, (9,9), 0)return frameBin另外,在代碼中我還加入了腐蝕膨脹和高斯濾波。這主要是在實(shí)驗(yàn)中發(fā)現(xiàn),當(dāng)黃桿離機(jī)器人比較遠(yuǎn)的時(shí)候,一些干擾物對(duì)檢測(cè)的影響很大,加入腐蝕膨脹可以很好地濾除大量不必要的干擾。當(dāng)然濾波器的大小也需要把握好,否則可能會(huì)把黃桿也濾除。
圖片經(jīng)上面的預(yù)處理后,會(huì)得到一張二值化圖。理想情況下,應(yīng)主要包含黃桿信息(可能包含一些顏色相似的噪聲)。為此我們需要在二值化圖像中找到黃桿所在的位置。由于我們比賽用的黃桿是長(zhǎng)條形,可以進(jìn)一步借助形狀信息來(lái)判斷。我在代碼中用的判別方法如下:
先在二值化圖像上進(jìn)行凸包檢測(cè),移除面積或周長(zhǎng)較小的凸包,找到剩下凸包的最小包圍矩形框,對(duì)每個(gè)外接矩形框進(jìn)行形狀判別,保留長(zhǎng)寬比符合要求的矩形框,最后在剩下的矩形框中選擇長(zhǎng)寬比最大的矩形框。代碼如下:
def _findStick(self, frameBin, minPerimeter, minArea):"""find the yellow stick in the preprocessed frame.Args:frameBin: preprocessed frame.minPerimeter: minimum perimeter of detected stick.minArea: minimum area of detected stick.Return: detected stick marked with rectangle or []."""rects = []_, contours, _ = cv2.findContours(frameBin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)if len(contours) == 0:return rectsfor contour in contours:perimeter = cv2.arcLength(contour, True)area = cv2.contourArea(contour)if perimeter>minPerimeter and area>minArea:x,y,w,h = cv2.boundingRect(contour)rects.append([x,y,w,h])if len(rects) == 0:return rectsrects = [rect for rect in rects if (1.0*rect[3]/rect[2])>0.8]if len(rects) == 0:return rectsrects = np.array(rects)print(rects)rect = rects[np.argmax(1.0*(rects[:,-1])/rects[:,-2]),]rect[1] += int(self._frameHeight *(1-self._cropKeep))return rect上面的代碼中還加入和很多判斷。目的是為了當(dāng)圖像中沒(méi)有我們需要的黃桿時(shí),直接返回一個(gè)空的列表。還有需要注意的一點(diǎn)是,最后我們是在經(jīng)過(guò)裁剪的圖像上進(jìn)行檢測(cè)的,因此還需要把最終的坐標(biāo)信息轉(zhuǎn)換的到原圖中。下面給出一個(gè)完整的黃桿檢測(cè)結(jié)果圖:
至此,我們已經(jīng)實(shí)現(xiàn)了NAO機(jī)器人高爾夫比賽中紅球和黃桿的識(shí)別和定位。其實(shí)比賽場(chǎng)地上還有其他的目標(biāo)需要檢測(cè),如白線、障礙物甚至球洞,這里就不多介紹。
寫(xiě)在最后的話:
感謝你一直讀到這里,希望本篇博客對(duì)你有點(diǎn)幫助。關(guān)于本篇博客中的任何問(wèn)題歡迎指出,虛心接受各位大佬的教導(dǎo)!
總結(jié)
以上是生活随笔為你收集整理的NAO机器人高尔夫中的视觉系统设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 原来,我连一个URL都写不对…
- 下一篇: Win系统集成一键显示隐藏系统文件到鼠标