基于websocket和opencv的水下机器人控制系统

项目简介

本次项目的目的是实现水下机器人的基本控制和特殊颜色物体识别。本次实验使用的水下机器人的ROV的一种。ROV,即遥控无人潜水器(Remote Operated Vehicle ),无人水下航行器(Unmanned Underwater Vehicle,UUV)的一种,系统组成一般包括:动力推进器、遥控电子通讯装置、 黑白或彩色摄像头、摄像俯仰云台、用户外围传感器接口、实时在线显示单元、导航定位装置、自动舵手导航单元、辅助照明灯和凯夫拉零浮力拖缆等单元部件。功能多种多样,不同类型的ROV用于执行不同的任务,被广泛应用于军队、海岸警卫、海事、海关、核电、水利、水电、海洋石油、渔业、海上救助、管线探测和海洋科学研究等各个领域。

ROV分为观察级和作业级。观察级ROV的核心部件是水下推进器和水下摄像系统,有时辅以导航、深度传感器等常规传感器。本体尺寸和重量较小,负荷较低。成本较低。作业级ROV用于水下打捞、水下施工等应用,尺寸较大,带有水下机械手、液压切割器等作业工具。造价高。

需求分析

本次实验需要实现的功能如下:

  • 完成与水下机器人的连接,能够获取水下机器人状态数据。
    • 这里需要使用利用websocket建立主机和机器人的通信,随时获取机器人的状态。
    • 这里需要的状态数据为水下机器人的三维坐标以及航向角,向水下机器人传输的数据为四个电机的转速参数以及内部树莓派的启动和关闭。
  • 完成获取水下机器人摄像机图像数据。
    • 这里需要使用python中的opencv函数库获取摄像头的数据,主要实现将实时的识别图像显示在pc上,同时获取已识别物体的中心坐标,传入pc,为机器人的pid算法提供数据基础。
  • 实现基本的控制水下机器人推进器和摄像机云台的功能。
    • 考虑到机器人的四个推进器在同一个参数情况下推进力不同,我们经过多次测试实现了机器人的平稳漂浮、旋转、前进、后退等基本单步控制,实现了推进器的需求。
  • 整体业务需求:水池内部前方放置红、黄、绿三个颜色的信标(悬挂,顺序可能会变化),要求水下机器人能够在程序的控制下,自主的运动至指定颜色的信标处,并接触信标。
  • 在本次实验中,需要使用pid算法满足水下机器人在控制下稳定、自主地到达信标。考虑到绳子的牵引作用,在行进过程中需要对机器人的行进方向进行调整。
  • 接触信标之后需要让水下机器人立刻停止,防止对信标和机器人造成影响。
  • 出发点要求:水下机器人位于水池信标的对侧,摄像机必须背对或侧对信标。
  • 初始化运动要求:水下机器人能够在电子罗盘的引导下,调整方向,旋转至正确的头部朝向信标的方向。
    • 这一部分,由于在具体实验中我们发现如何调整向水下机器人传输参数,机器人都很难旋转至正确方向,这里我们使用pid算法后,情况有所缓解。但受到绳子和水底地理环境的影响,还是很难精准地确定方向,因此我们决定确定大致方向后出发,在后续运动过程中逐步调节运动方向。
  • 水下机器人在运动过程中,逐渐寻找目标;找到指定颜色后,前进并接触信标。
  • 图像识别功能在计算机端完成,代码和报告中必须体现。
    • 代码见附录。
  • 运动过程不可以使用单步行进方式,必须使用PID算法,代码和报告中必须体现。
    • 这里的pid算法使用的是python的simple_pid库。
  • 测试环节必须包含随机角度出发的测试用例(背对3次,侧方向3次),并统计每次完成时间,在报告中必须体现测试环节和测试结果。
    • 测试阶段分别对黄色信标和绿色信标完成了追踪和识别实验。

系统实现的基本流程图:

总体设计与技术背景

实现的系统类图如下:

使用的技术如下:

基于websocket协议的机器人通信、基于opencv的图像获取、视频流的传输获取、基于simple_pid的控制算法。

**基于websocket协议的机器人通信:**相较于socket协议的传输,我们使用了更优的websocket技术建立我们的通信。WebSocket同HTTP一样也是应用层的协议,但是它是一种双向通信协议,是建立在TCP之上的。它在连接过程,即握手过程中,浏览器、服务器建立TCP连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(开始前的HTTP握手)。服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。当收到了连接成功的消息后,通过TCP通道进行传输通信。Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。TCP连接则更依靠于底层的IP协议,IP协议的连接则依赖于链路层等更低层次。WebSocket则是一个典型的应用层协议。Socket是传输控制层协议,WebSocket是应用层协议。

**基于opencv的图像获取:**opencv常用来进行计算机视觉相关的作业。计算机视觉大体可以分为以下几类:检测、识别、分类、定位、测量。说得具体一点,检测表面划痕、异物检测,检测某个对象是否存在,检测是否完整等;识别人脸、识别车牌、识别字符、识别某个标志等图像上的对象;对图像上的对象进行分类,对混合在一起的产品进行分类;定位检测对象在图像中的位置,从而给机器人或者别的机构传递位置信息;测量图像中某个对象的尺寸。OpenCV(Open Source Computer Vision Library)开放源代码计算机视觉库,主要算法涉及图像处理、计算机视觉和机器学习相关方法。OpenCV 其实就是一堆 C 和 C++语言的源代码文件,这些源代码文件中实现了许多常用的计算机视觉算法。OpenCV 可用于开发实时的图像处理、计算机视觉以及模式识别程序。

OpenCV由一系列C函数和C类构成,它有C,C,Python和java接口,当前SDK(Software Development Kit软件开发工具包) 已经支持C++、Java、Python等语言应用开发, 当前OpenCV本身新开发的算法和模块接口都是基于C++产生。其覆盖了工业产品检测、医学成像、无人机飞行、无人驾驶、安防、 卫星地图与电子地图拼接、信息安全、用户界面、摄像机标定、立体视觉和机器人等计算机视觉应用领域。以下是opencv的核心模块:

图像处理(Image Processing)是用计算机对图像进行分析,以达到所需结果的技术。图像处理技术一般包括图像压缩,增强和复原,匹配、描述和识别3个部分。数字图像处理(Digital Image Processing)是通过计算机对图像进行去除噪声、增强、复原、分割、提取特征等处理的方法和技术。数字图像是指用工业相机、摄像机、扫描仪等设备经过拍摄得到的一个大的二维数组,该数组的元素称为像素,其值称为灰度值。

计算机视觉(Computer Vision)是指用摄像机和电脑代替人眼对目标进行识别、跟踪和测量等机器视觉,并进一步做图形处理,使之成为更适合人眼观察或仪器检测图像的一门学科。

OpenCV是最初由英特尔公司发起并开发,以 BSD 许可证授权发行,可以在商业和研究领域中免费使用,现在美国机器人公司Willow Garage 为 OpenCV 提供主要的支持。主要贡献人物李信弘( Shinn Lee)、Vadim Pisarevsky、Gary Bradski。 (其他付费的机器视觉开发包Matlab、Halcon、VisonPro、Sapera、EVision)。在本次实验中利用opencv实现图像处理,同时根据实时的识别数据确定信标的位置,从而获取pid的基本参数——航向角。

视频流的获取:只是获取opencv处理的图像是不足够的,我们需要获取实时连续的视频流,因此需要进行视频流的获取。 由于视频信息十分丰富且信息量大,而当今网络的传输速度慢,如果按传统的计算机文件的处理方式来处理网络视频数据信息,将会造成麻烦。通常情况下,计算机处理文件是完整地进行处理的,也就是说文件在被处理的时候必须是一个完整的整体。文件一旦遭到损坏,或者只有一半的内容,那么计算机将认为该文件是坏的,是不可处理的。如果这套原则也同时适用于网络视频文件的话,观赏者至少得等数十分钟到数小时,等视频文件下载完后才能看到,这显然让人难以接受。

解决的办法是采用一种专用的流体化技术提取文件。这种流体化技术的原理是这样的:服务器在向用户传输视频文件时,不是一次将文件整体发送出去,而是先按播放的时间顺序将其分为小的片断,类似于图像中的帧,然后将这些片断依次发给用户。用户的网络播放工具接收到这些片断后,连续播放这些片断,就可以产生完整的声音和图像,只是开始时有些延迟。网上直播时视频文件的产生也是不断生成新的片断。为了保证声音、图像的播放效果,服务器与用户计算机间的网络传输速度有一定的要求。如果网络传输速度较慢,播放时就会出现断断续续的现象。应用中可以根据用户的实际带宽,提供用户不同清晰度的播放效果。这就是视频流技术。

在本次实验中,可以使用websocket实现实时视频流的显示,通过以上的操作可以使获得的视频更加流畅,不存在卡顿问题。

基于simple_pid的控制算法:simple_pid是基于simple_pid的控制算法,为了摆脱外部的依赖,我们使用了这个库函数。用法非常简单:

from simple_pid import PID
pid = PID(1, 0.1, 0.05, setpoint=1)

# Assume we have a system we want to control in controlled_system
v = controlled_system.update(0)

while True:
    # Compute new output from the PID according to the systems current value
    control = pid(v)

    # Feed the PID output to the system and get its current value
v = controlled_system.update(control)

安装的命令如下:pip install simple-pid

使用的话,只需像这样调用对象:

PID__call__()

output = pid(current_value)

PID 在定期更新时效果最佳。为此,应当设置为每次更新之间应有的时间量,然后在程序循环中每次调用 PID。只有在几秒钟过去后才会计算新的输出:

sample_timesample_time

pid.sample_time = 0.01  # Update every 0.01 seconds
while True:
  output = pid(current_value)

设置设定值,即PID试图实现的值,只需这样设置:

pid.setpoint = 10

当PID运行时,可以随时更改调谐。它们可以单独设置,也可以一次全部设置:

pid.Ki = 1.0

pid.tunings = (1.0, 0.2, 0.4)

要在反向模式下使用 PID,这意味着输入的增加会导致输出的减少(例如冷却时),您可以将调谐设置为负值:

pid.tunings = (-1.0, -0.1, 0)

为了获得一定范围内的输出值,并避免积分收尾(因为积分项永远不会超出这些限制),可以将输出限制在一个范围内:

pid.output_limits = (0, 10) # Output value will be between 0 and 10

pid.output_limits = (0, None) # Output will always be above 0, but with no upper bound

要禁用 PID 以便不计算新值,请将自动模式设置为 False:

pid.auto_mode = False # No new values will be computed when pid is called

pid.auto_mode = True # pid is enabled again

禁用 PID 并手动控制系统时,告诉 PID 控制器在将控制权交还给它时从哪里开始可能会很有用。这可以通过启用自动模式来完成,如下所示:

pid.set_auto_mode(True, last_output=8.0)

这会将 I 项设置为给定的值,这意味着如果正在控制的系统在该输出值上稳定,则 PID 将从该点启动时保持系统稳定,并且在重新打开 PID 时输出中没有任何大的颠簸。last_output

调谐 PID 时,查看每个组件如何对输出做出贡献会很有用。它们可以这样看:

p, i, d = pid.components # The separate terms are now in p, i, d

为了消除某些类型系统中的过冲,可以直接在测量上计算比例项,而不是误差。可以像这样启用:

pid.proportional_on_measurement = True

要在对错误值执行任何计算之前将其转换为另一个域,可以向 PID 提供回调函数。回调函数应该接受一个参数,该参数是来自设定值的误差。例如,这可用于在偏航角控制中获取值介于 [-pi, pi] 之间的度值误差:error_map

import math
def pi_clip(angle):
    if angle > 0:
        if angle > math.pi:
            return angle - 2*math.pi
    else:
        if angle < -math.pi:
            return angle + 2*math.pi
    return angle
pid.error_map = pi_clip

详细设计与实现

cv_config_video模块

初始化函数

​ 将初始的(x,y)和(xx,yy)进行设定,其中x,y为显示屏幕的中心位置,xx、yy分别为特殊颜色物体识别之后的中心位置的坐标。

视频流获取和基本的信标识别

这里首先创建一个窗口,名字叫做Window,然后传入摄像头的IP地址,http://用户名:密码@IP地址:端口/,创建一个VideoCapture。确认摄像头已开启的情况下,显示缓存、

调节摄像头分辨率、设置FPS。

接下来进入循环,逐帧捕获,判断是否读取到图片。根据读取到的图片,进行高斯模糊、转换演的空间、定义特殊颜色的无图HSV阈值、对图片进行二值化处理、腐蚀、膨胀消除噪声、寻找图中轮廓等一系列操作获取特殊颜色物体的分布范围。

如果存在至少一个轮廓则进行如下操作,即找到面积最大的轮廓、使用最小外接圆圈出面积最大的轮廓、计算轮廓的矩和重心,处理半径大于5的轮廓(因此特殊颜色物体识别只关心近距离的识别)。之后,在屏幕上划出最小外接圆,保存此时物体中心点的坐标。

相关代码如下:

def video_config(self):
        # 创建一个窗口 名字叫做Window
        cv2.namedWindow('Window', flags=cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO | cv2.WINDOW_GUI_EXPANDED)

        # 摄像头的IP地址,http://用户名:密码@IP地址:端口/
        ip_camera_url = 'http://192.168.137.2:8888/video_feed0'
        # 创建一个VideoCapture
        cap = cv2.VideoCapture(ip_camera_url)

        print('IP摄像头是否开启: {}'.format(cap.isOpened()))

        # 显示缓存
        print(cap.get(cv2.CAP_PROP_BUFFERSIZE))
        cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

        # 调节摄像头分辨率
        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

        print(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        print(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

        # 设置FPS
        print('setfps', cap.set(cv2.CAP_PROP_FPS, 25))
        print(cap.get(cv2.CAP_PROP_FPS))

        while True:
            # 逐帧捕获
            ret, frame = cap.read()  # 第一个参数返回一个布尔值(True/False),代表有没有读取到图片;第二个参数表示截取到一帧的图片
            # gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            cv2.imshow('Window', frame)

            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
            frame = imutils.resize(frame, width=500)
            # 进行高斯模糊
            blurred = cv2.GaussianBlur(frame, (11, 11), 0)
            # 转换颜色空间到HSV
            hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV)
            # 定义黄色无图的HSV阈值
            lower_yellow = np.array([0, 43, 46])
            upper_yellow = np.array([10, 255, 255])
            # 对图片进行二值化处理
            mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
            # 腐蚀操作
            mask = cv2.erode(mask, None, iterations=7)
            # 膨胀操作,先腐蚀后膨胀以滤除噪声
            mask = cv2.dilate(mask, None, iterations=2)
            # 寻找图中轮廓
            cnts, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

            # 如果存在至少一个轮廓则进行如下操作
            if len(cnts) > 0:
                # 找到面积最大的轮廓
                c = max(cnts, key=cv2.contourArea)
                # 使用最小外接圆圈出面积最大的轮廓
                ((x, y), radius) = cv2.minEnclosingCircle(c)
                # 计算轮廓的矩
                M = cv2.moments(c)
                # 计算轮廓的重心
                center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
                # 只处理尺寸足够大的轮廓
                if radius > 5:
                    # 画出最小外接圆
                    cv2.circle(frame, (int(x), int(y)), int(radius), (0, 255, 255), 2)
                    # 画出重心
                    cv2.circle(frame, center, 5, (0, 0, 255), -1)
                    self.x = int(x)
                    self.y = int(y)
                max_contour = sorted(cnts, key=cv2.contourArea)[-1]
                self.get_sign_angle(self,np.mean(max_contour[:, :, 0]), np.sum(mask))  

检测识别物体并返回坐标

查看是否检测到物体,当获取的mask超过10时,认为检测到目标,同时保存位置信息,以便pid算法的计算。

整体代码如下:

def get_sign_angle(self, position, cre):
    if cre > 10:
        print("检测到目标")
        global po_x
        global po_y
        po_x = self.x
        po_y = self.y
    else:
        print("未检测到目标")

水下机器人控制模块

这一部分需要实现对机器人的控制,确定机器人的单步动作,即上浮、转动、前进等等,同时在旋转过程中应用了pid算法,基本实现方向的纠正,得到较为满意的结果。需要注意的是,pid中传入的三个参数需要进行多次试验才能得到较好的结果。
相关代码如下:

async def begin():
    async with websockets.connect(uri) as ws:
        await ws.send("{}:{}:{}:{}:{}".format(1550, 1510, 1675, 1575, 0))
        data = await ws.recv()
        r = data.split(':', -1)[-1]
        print(r)
        time.sleep(0.02)


async def turn():
    i = 0
    target = 268.0
    async with websockets.connect(uri) as ws:
        while i == 0:
            temp = 1550
            await ws.send("{}:{}:{}:{}:{}".format(temp, temp, 1675, 1575, 0))
            data = await ws.recv()
            r = data.split(':', -1)[-1]
            if float(r) < 268.0:
                pid = PID(1, 0.01, 0.1, setpoint=target)
                v = pid(float(r))
                temp = temp + v
                await ws.send("{}:{}:{}:{}:{}".format(temp, temp, 1675, 1575, 0))
                data = await ws.recv()
                r = data.split(':', -1)[-1]
                print(r)
            elif float(r) > 268.0:
                pid = PID(1, 0.01, 0.1, setpoint=target)
                v = pid(float(r))
                temp = temp + v
                await ws.send("{}:{}:{}:{}:{}".format(temp + v, temp, 1675, 1575, 0))
                data = await ws.recv()
                r = data.split(':', -1)[-1]
                print(r)
            elif float(r) == 268.0:
                return 0
        time.sleep(0.02)


async def stop():
    async with websockets.connect(uri) as ws:
        await ws.send("{}:{}:{}:{}:{}".format(1530, 1530, 1530, 1530, 0))
        data = await ws.recv()
        print(data)
        time.sleep(0.02)


async def forward():
    async with websockets.connect(uri) as ws:
        await ws.send("{}:{}:{}:{}:{}".format(1485, 1575, 1675, 1575, 0))
        data = await ws.recv()
        r = data.split(':', -1)
        time.sleep(0.02)

近距离识别模块

这里需要我们获取特殊颜色物体中心点的坐标,将数据传入控制中的转向模块,实现近距离的识别和获取。

系统测试

测试结果如视频所示,我们分别进行了背对3次,侧方向3次的测试,分别识别黄色信标和绿色信标(考虑到红色信标和对角的距离太大,电线的影响较多,因此没有加入测试),最后都得到了较好的结果。下表为测试结果:

方向信标颜色时间
背对黄色46秒
背对黄色37秒
背对黄色38秒
侧对绿色39秒
侧对绿色32秒
侧对黄色33秒

设计总结

​ 综上所述,整体项目开发较为完备,基本实现了之前的客户需求,在实验过程中遇到了许多难点:电线对于机器人的牵引和阻碍作用、水下地理环境对机器人平衡和前进的影响、控制命令的影响、不同推进器同一参数推力不同、长时间使用机器人会导致航向角不发生变化等等。我们通过多次试验最终克服了这些困难:放置电线时控制长度、减少机器人向上的推力用以减少绳子的阻碍作用;控制好上下推进器的推力以避免、水下地理环境对机器人平衡和前进的影响;控制命令需要注意实现的次数,熟练应用异步编程;多次调节参数获取平衡和平稳前进的参数条件;控制实验时间来避免实验因航向角变化而中断。之后这篇报告也会在课程结束后放在我的博客上,希望以后的学弟学妹能够避免这些问题。

​ 这是我选修的第二门水下选修课(因为机器人视觉太火爆,没抢上),显然在使用opencv上没有很熟练。现在回头看看,我的代码并不健全,还有很多的不足和提升空间,我应该在之后的课程中提升自己的代码能力。作为代码的编写者,我深刻认识到了合作的重要性。没有成员的陪伴测试,我想我编出了程序也会花很久的时间调试。感谢我的队友给我的支持与鼓励。行文至此,我想趁着本科时光走入后半场这个时间段,创造更多的code,建立更多属于team的项目。愿我且歌且行!


   转载规则


《基于websocket和opencv的水下机器人控制系统》 xhsioi 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
每日一题整理——2023.3 每日一题整理——2023.3
每日一题整理——2023.3 2023.1.3 矩阵中的局部最大值 数据范围: 我的思路:暴力模拟,做的依托答辩 class Solution { public: vector<vector<int>>
2023-04-17
下一篇 
华为软件精英挑战赛——普朗克计划 华为软件精英挑战赛——普朗克计划
华为软件精英挑战赛——普朗克计划 2023华为软件精英挑战赛——普朗克计划 2023/3/26 https://competition.huaweicloud.com/codecraft2023 云智能机器人协同调度优化 背景信息 机
2023-04-15
  目录