
2023年全国大学生电子设计大赛E题视觉思路
2023年全国大学生电子设计大赛E题视觉思路
这些都是我和队友们茶不思夜不寐苦想出来的 *** 转载请注明来源 ***
背景
第一次玩视觉和python,整完智能车只有十几天的时间学。
 可能有些地方不太对,欢迎各位大佬指正。
 题目要求详见2023年TI杯>>E题-运动目标控制与自动追踪系统
选型
选型这块没什么好说的,市面上的视觉处理都能用。OpenMV,k210,v831,树莓派都可以。
 K210由于没有USB口,链接IDE调试的时候帧率极低,实际效果需要实测才能知道。只是追踪色块还是够用的。
设计思路
- 按组委会的问答看,摄像头是随意可以随意摆放的,保持激光和屏幕的距离为一米即可。我的思路是红点跟着摄像头一起动,绿点静止。
 - 激光的斑点大小小于等于1cm,光晕不算在内,激光笔的光晕可以大点,方便识别。加上滤光片可以减小环境光的影响。
 - 摄像头的视角尽量把整个屏幕看到特别的静止的摄像头,最理想的情况是摄像头的视角刚好是整个屏幕,可以减少外界堆识别的影响。
 - 黑色胶带会吸光,这点需要考虑在内。任何光线影响都是可以通过调整曝光解决的,可以试着手动调节曝光,达到理想的效果
 - 云台的可控精度一定要高,这是整个题的关键,精度不高说啥都没用。
 
视觉处理
这次视觉部分比较简单,只要寻找色块和四边形即可。可以不涉及机器学习。
 1、有关基础第一题和第二题:对摄像头要求很高,如果没有高清晰度的摄像头我建议开环。第二题确实能看到铅笔画的框可以用边缘检测+find_rects(寻找矩形)或者fine_line(巡直线)
 2、基础部分三次题1.8cm宽的胶带找出来很方便,要招红色斑点得调曝光值,当然如果滤光片效果好也是可以的。
 3、发挥部分主要是招色块了这部分用find_blobs都能解决。
解题思路
手上资源有限,我们手上只有一个OpenMV H7和一个V831,我选择用更熟悉的OpenMV作为主处理,V831来找两个激光,毕竟v831的摄像头要好的多,容易找。
 关于基础第三四题的思路,我们采用半开环。
 第三题,摄像头跟着云台动,用摄像头找到四边形第一个角,然后写死。
 第四题,摄像头跟着云台动,用摄像头找到四边形第一个角,然后划线到第二个角以此类推。
 摄像头的中心点就是红色激光笔打在屏幕上的地方,理论上是可行的。但实际上云台在动的时候摄像头会抖,导致这题分就被扣完了。所以为了保险再找个红点修正误差。或者通过相似三角形把图像的坐标映射到屏幕上。加个陀螺仪然后写死比例。
V831处理处理矩形和红点参考
V831可以参考这篇Neucrack-关于2023年电赛E题的简单思考
 所说的maix II DOCK就是v831
OpenMV处理矩形和红点参考
使用OpenMV就比较简单了,把曝光值一调,红绿点就找出来了,如果想要同时识别矩形,就得把曝光值调回来,不然会一片黑。
 可以采用状态机的思想来做,下面是我的shi山代码,思路可以做参考,多少会有点问题,不要直接cv。
 下面的思路就是找到第一个点,然后云台写死比例,让云台绕着算出来的路径划线。
import sensor, image, time
from pyb import UART
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)  # H7 Plus可以用QVGA看的更清楚
sensor.set_vflip(True)              # 图像镜像
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
uart = UART(3, 115200)              # 串口初始化
clock = time.clock()
# 红色的LAB阈值
THRESHOLD = (6, 98, 5, 39, -3, 18)
# 屏幕中心点对应的图像中坐标
center_point = (80, 37)
# 我需要给队友发的几个点
first_point = [0, 0]
second_point = [0, 0]
last_point  = [0, 0]
# 状态机
state = 0
# 红点位置
red = 0
redpoint = [0,0]
# 临时变量
pian_x0 = [99,99,99]
pian_y0 = [99,99,99]
i=0
def area(x1, y1, x2, y2, x3, y3):
    """计算三角形面积"""
    return abs((x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2)) / 2.0)
def is_inside_polygon(x1, y1, x2, y2, x3, y3, x4, y4, cx, cy):
    """检查点(cx, cy)是否在四边形内部"""
    # 计算四边形的面积
    total_area = area(x1, y1, x2, y2, x3, y3) + area(x1, y1, x4, y4, x3, y3)
    # 计算点和四边形各个顶点构成的四个三角形的面积
    a1 = area(cx, cy, x1, y1, x2, y2)
    a2 = area(cx, cy, x2, y2, x3, y3)
    a3 = area(cx, cy, x3, y3, x4, y4)
    a4 = area(cx, cy, x4, y4, x1, y1)
    # 如果这四个三角形的面积之和等于四边形的面积,则点在四边形内部;否则,点在四边形外部
    return total_area == a1 + a2 + a3 + a4
# 找最大色块
def find_max(blobs):
    max_size=0
    for blob in blobs:
        if blob[2]*blob[3] > max_size:
            max_blob=blob
            max_size = blob[2]*blob[3]
    return max_blob
while(True):
    clock.tick()
    if state != 2:  # 状态2是找红点的,所以状态2不能进入下面的程序
        img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0) ## 畸变矫正,就是关掉摄像头的鱼眼效果
        # 找矩形
        for r in img.find_rects(threshold = 10000):
            img.draw_rectangle(r.rect(), color = (255, 0, 0))
            if r.magnitude() > 20000 and r.magnitude() < 60000:
                for p in r.corners(): img.draw_circle(p[0], p[1], 3, color = (0, 255, 0))
            if state == 0:
                if r.magnitude() > 20000 and r.magnitude() < 60000:
                    pianx = int(r.corners()[3][0])+3-int(center_point[0])
                    piany = int(r.corners()[3][1])+3-int(center_point[1])
                    print("pianx:%4d  piany:%4d  main:%4d\r\n" % (pianx, piany, r.magnitude()))
                    # 把第一个点的偏差发给主控
                    uart.write("pianx:%4d  piany:%4d\r\n" % (pianx, piany))
                    pian_x0[i] = pianx
                    pian_y0[i] = piany
                    i = i+ 1
                    if i>=3:
                        i=0
    #                print("x1:%4d  y1:%4d  x2:%4d  y2:%4d  x3:%4d  y3:%4d x00:%4d x01:%4d x02:%4d\r\n"%(first_point[0], \
    #                    first_point[1], second_point[0], second_point[1], last_point[0], last_point[1], \
    #                    pian_x0[0],pian_x0[1],pian_x0[2]))
                    # 因为不是狠熟悉python的用法,所以就简单粗暴点,整个是检测到激光是否稳定打到第一个点
                    if pian_x0[0] ==0 and pian_x0[1] == 0 and pian_x0[2] == 0 and pian_y0[0] == 0 and pian_y0[1] == 0 and pian_y0[2] == 0:
                        # 满足就进入状态1
                        state = 1
                        first_point[0] = int(r.corners()[3][0])+1
                        first_point[1] = int(r.corners()[3][1])+1
                        second_point[0] = int(r.corners()[2][0])-1
                        second_point[1] = int(r.corners()[2][1])+1
                        last_point[0]  = int(r.corners()[1][0])-1
                        last_point[1]  = int(r.corners()[1][1])-1
                        time.sleep_ms(500)
            # 状态1: 把剩下的三个点发给主控,实现半闭环
            if state == 1: 
                uart.write("x1:%4d  y1:%4d  x2:%4d  y2:%4d  x3:%4d  y3:%4d\r\n"%(first_point[0], \
                          first_point[1], second_point[0], second_point[1], last_point[0], last_point[1]))
                print("x1:%4d  y1:%4d  x2:%4d  y2:%4d  x3:%4d  y3:%4d\r\n"%(first_point[0], \
                first_point[1], second_point[0], second_point[1], last_point[0], last_point[1]))
                time.sleep_ms(500)
                state = 2   # 发完以后进入状态2
    # 状态2: 降低曝光,找红点
    if state == 2:
        # 调整增益和曝光值
        sensor.set_auto_gain(False)
        sensor.set_auto_exposure(False, exposure_us=1400)
        sensor.set_auto_whitebal(False)
        img2 = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0)
        blobs = img2.find_blobs([THRESHOLD])    # 找红点
        if blobs:
            max_blob = find_max(blobs)
            redpoint[0]=max_blob.cx()
            redpoint[1]=max_blob.cy()
            # 限幅,在屏幕外面的红点就不发给主控
            if 20 < redpoint[0] < 140 and 10 < redpoint[1] < 70:
                img2.draw_cross(max_blob.cx(), max_blob.cy(), color=(0, 255, 128))
                print("cx: %4d  cy: %4d"%(max_blob.cx(),max_blob.cy()))
                uart.write("cx:%4d  cy:%4d\r\n"%(redpoint[0],redpoint[1]))
        else:
            red = 0
        uart.write("red:%2d\r\n" % red)
        state = 3   # 转到状态3
    # 状态3: 继续找矩形,因为是半闭环这里主要是用来判断红点是否出界
    if state == 3:
        # 把增益和曝光调回来
        sensor.set_auto_gain(True)
        sensor.set_auto_exposure(True)
        sensor.set_auto_whitebal(False)
        img3 = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0) ## 畸变矫正
        for r in img3.find_rects(threshold = 10000):
            img3.draw_rectangle(r.rect(), color = (255, 0, 0))
            if r.magnitude() > 20000 and r.magnitude() < 60000:
                for p in r.corners(): img3.draw_circle(p[0], p[1], 3, color = (0, 255, 0))
        # 检测红点在不在矩形里面,在里面为1,外面为-1
        if is_inside_polygon(r.corners()[3][0],r.corners()[3][1], r.corners()[2][0],r.corners()[2][1],\
                             r.corners()[1][0],r.corners()[1][1], r.corners()[0][0],r.corners()[0][1],\
                             redpoint[0],redpoint[1]):
#            print("True")
            red = 1
        else:
#            print("False")
            red = -1
        uart.write("red:%2d\r\n" % red)
#        print("red: %2d" % red)
#        print(abs(r.corners()[3][0]-redpoint[0]),abs(r.corners()[3][1]-redpoint[1]))
        # 检测到红点差不多打回第一个点就退出状态,否则回去找红点
        if abs(r.corners()[3][0]-center_point[0])<13 and abs(r.corners()[3][1]-center_point[1])<15:
            state = 0
        else:
            state = 2上面代码经过验证是可行的,就是有玄学成分。
 下面代码思路是找到第一个点,然后慢慢划线到第二个点,然后第三个,第四个以此类推。(未经过验证,只是理论)
import sensor, image, time
from pyb import UART
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)  # H7 Plus可以用QVGA看的更清楚
sensor.set_vflip(True)              # 图像镜像
sensor.set_hmirror(True)
sensor.skip_frames(time = 2000)
uart = UART(3, 115200)              # 串口初始化
clock = time.clock()
# 屏幕中心点对应的图像中坐标
center_point = (80, 40)
# 状态机
state = 0
# 记录四个点的坐标
rect_points = []
# 记录面积
area = 0
while(True):
    clock.tick()
    img = sensor.snapshot().lens_corr(strength = 1.8, zoom = 1.0) ## 畸变矫正,就是关掉摄像头的鱼眼效果
    # 找矩形
    for r in img.find_rects(threshold = 10000):
        # 画出找到的矩形
        img.draw_rectangle(r.rect(), color = (255, 0, 0))
        # 记录面积
        area = r.magnitude()
        # 限制矩形大小,太大太小抖滤掉
        if area > 20000 and area < 53000:
            # 用圆出四个角
            rect_points = []
            for p in r.corners():
                img.draw_circle(p[0], p[1], 3, color = (0, 255, 0))
                rect_points.append(p)   # 记录坐标点
    # 状态0: 找第一个角
    if state == 0:
        err_p1=(rect_points[3][0]-center_point[0],rect_points[3][1]-center_point[1])
        if area > 20000 and area < 53000:
            print("p1_x:%4d  p1_y:%4d\r\n" % (err_p1[0], err_p1[1]))
            # 把第一个点的偏差发给主控
            uart.write("p1_x:%4d  p1_y:%4d\r\n" % (err_p1[0], err_p1[1]))
        if err_p1[0] < 2 and err_p1[1] < 2:
            state = 1
    # 状态1: 找第二个角
    if state == 1:
        err_p2=(rect_points[2][0]-center_point[0],rect_points[2][1]-center_point[1])
        if area > 20000 and area < 53000:
            print("p2_x:%4d  p2_y:%4d\r\n" % (err_p2[0], err_p2[1]))
            # 把第二个点的偏差发给主控
            uart.write("p2_x:%4d  p2_y:%4d\r\n" % (err_p2[0], err_p2[1]))
        if err_p2[0] < 2 and err_p2[1] < 2:
            state = 2
    # 状态2: 找第三个角
    if state == 2:
        err_p3=(rect_points[1][0]-center_point[0],rect_points[1][1]-center_point[1])
        if area > 20000 and area < 53000:
            print("p3_x:%4d  p3_y:%4d\r\n" % (err_p3[0], err_p3[1]))
            # 把第三个点的偏差发给主控
            uart.write("p3_x:%4d  p3_y:%4d\r\n" % (err_p3[0], err_p3[1]))
        if err_p3[0] < 2 and err_p3[1] < 2:
            state = 3
    # 状态3: 找第四个角
    if state == 3:
        err_p4=(rect_points[0][0]-center_point[0],rect_points[0][1]-center_point[1])
        if area > 20000 and area < 53000:
            print("p4_x:%4d  p4_y:%4d\r\n" % (err_p4[0], err_p4[1]))
            # 把第四个点的偏差发给主控
            uart.write("p4_x:%4d  p4_y:%4d\r\n" % (err_p4[0], err_p4[1]))
        if err_p4[0] < 2 and err_p4[1] < 2:
            state = 0*** 上述代码未经验证,绝对会有逻辑错误,仅提供思路 ***
V831寻找色块参考
我V831搭载的摄像头不错,不调曝光也能看到红点和绿点。
 这段其实没什么好说的,都是官方例程,直接上代码吧。
from maix import image, display, camera
import serial
ser = serial.Serial("/dev/ttyS1",115200)  # 串口初始化
green = [(59, 100, -27, -5, -128, 127)]   # [绿光点大点(69, 86, -50, -32, -51, 70) 小点[(59, 100, -27, -5, -128, 127)]]
red   = [(52,  77,  10,  39, -14,   8)]   # [红光点(68, 97, 17, 42, -21, 41)]  [(61, 75, 18, 39, 0, 17)]
camera.camera.config(size=(240, 240))
grn_point = (0,0)
red_point = (0,0)
while True:
    img = camera.capture()
    g_blobs = img.find_blobs(green, merge=True)    #在图片中查找lab阈值内的颜色色块 merge 合并小框。
    r_blobs = img.find_blobs(red  , merge=True)
    if g_blobs:
        for i in g_blobs:
            grn_point = (int(i["centroid_x"]),int(i["centroid_y"]))
            img.draw_rectangle(i["x"], i["y"], i["x"] + i["w"], i["y"] + i["h"], 
                               color=(0, 0, 255), thickness=1) #将找到的颜色区域画出来
            # print("g_cx:%4d , g_cy:%4d" % (grn_point[0],grn_point[1]))
    if r_blobs:
        for j in r_blobs:
            red_point = (int(j["centroid_x"]),int(j["centroid_y"]))
            img.draw_rectangle(j["x"], j["y"], j["x"] + j["w"], j["y"] + j["h"], 
                               color=(255, 0, 0), thickness=1) #将找到的颜色区域画出来
            # print("r_cx:%4d , r_cy:%4d" % (red_point[0],red_point[1]))
    ser.write(b"err_x:%4d  err_y:%4d\r\n" % ((red_point[0]-grn_point[0]),(red_point[1]-grn_point[1])))
    display.show(img)V831调整增益和曝光
如果摄像头实在看不到红点和绿点可以和openMV一样调整增益和曝光
 原文:sipeed wiki
from maix import camera, display, image
camera.config(size=(224, 224))
exp, gain = 16, 16 # 初值,exp 曝光[0, 65536],gain 增益[16 - 1024],随意设置得值会受到驱动限制。
for i in range(120):
    exp, gain = exp + 32, gain + 16
    camera.config(exp_gain=(exp, gain))
    img = camera.capture()
    display.show(img)
camera.config(exp_gain=(0, 0)) # 设置为 0, 0 表示放弃控制恢复成自动曝光。如果需要旋转图像可以参考sipeed wiki
树莓派(opencv)寻找色块和矩形参考
我们对树莓派和opencv了解不深,不过可以参考一下这篇博客
 里面说的挺简洁的
总结
其实今年这题就纯比硬件,高精度决定上限,题目虽然看起来不难,但是要求的精度特别高。
 对于云台和摄像头来说都是挑战,像我们这些穷小子是玩不来的(泣了)
