一、图像处理方案

Opencv: 开源计算机视觉处理库,使用嵌入式SOC板卡作为摄像头模块,提供相对可靠的性能。

由于激光点的亮度远高于其他位置,所以采用HSV格式的色彩空间,对图像H,S,V域进行识别。

若要区分红绿激光,那么就对图像的RGB空间进行处理

2. 电工胶带的识别

  • 使用原图像->灰度图像->设置阈值后进行二值化的方法,使得黑胶带区域识别为纯白色,其余部分全部为纯黑色区域。关于调参:建议使用创建滑块窗口的方法,进行动态调参,减少调参上浪费的时间。

  • 摄像头容易受到外接环境光的干扰,可以通过调节分辨率,设置摄像头的图像获取范围,也有一种更为实用的方法:ROI(感兴趣区),可以通过创建掩膜的方式,只对ROI区域进行处理,这样就排除了外界环境的干扰。当然也可以使用切片的方法,定义一个变量用于读取图像的指定范围,然后再进行图像处理,需要显示参数时,再将一些特征点、辅助线、坐标点、数据显示在想要观看的图像上,使用opencv中的imshow进行可视化显示(注意:imshow是非常耗时的操作,在调试时可以使用,在最终运行时建议注释掉,提升程序执行效率,或者采取图像编解码的方法,使用TCP/UDP协议,ROS通信等方式进行远程图传,降低软件资源的使用)

  • ROI区域也不一定是适宜的图像处理区域,也可能有一些噪声信号的干扰,这可能就要需要用到一些滤波处理的算法,如:高斯平滑滤波(高斯模糊)。

  • 胶带容易受激光点的影响,导致二值化图像被==“分割”==,所以需要对胶带的二值化图像先膨胀,后进行图形学闭运算缝合胶带二值化图像漏洞

3. 串口通信的关键问题

  • 数据帧:一开始采用字符串发送地方法。结果单片机很容易接收不到一些数据帧
  • 通信速率:19200

3. 激光“巡线”的路径规划

  • 第一种方法:程序不断对图像进行处理,边缘检测,封闭图形检测,然后激光沿着检测出来的图形边沿进行巡线

  • 第二种方法:采用“路径规划的方法”,进行一定时间的图像采集后,确定好激光的运行路径,这样,即避免了激光对矩形框识别的强光干扰,又大大减少了程序运行的资源损耗,只需要执行激光跟随部分的程序,不需要再对图像进行边沿检测。

二、程序设计

视觉部分

1. 摄像头处理部分

1.1 摄像头的类封装

GuideLine 的作用:绘制辅助线

思考:Python中函数对形参的调用,会不会影响到实参?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 摄像头类创建
class pi_Camera():
# 类的初始化
def __init__(self):
# 图像初始化配置
self.Video = cv2.VideoCapture(8, cv2.CAP_V4L2)# 使能摄像头8的驱动

# 检查摄像头是否打开
ret = self.Video.isOpened()
if ret:
print("The video is opened.")
else:
print("No video.")

codec = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')
self.Video.set(cv2.CAP_PROP_FOURCC, codec)
self.Video.set(cv2.CAP_PROP_FPS, 60) # 帧数
self.Video.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 列 宽度
self.Video.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 行 高度

# 绘制辅助线
def GuideLine(self, c1, c2):
ret, image = self.Video.read()# 注意:read返回一个bull值和图像数据list!,需要用两个变量获取
if ret:
cv2.line(image, (0, 360), (640, 360), color=(0, 0, 255), thickness=3)# 红色的线
cv2.line(image, (0, 240), (640, 240), color=(0, 0, 255), thickness=3)# 红色的线
cv2.line(image, (int(c1), 360), (int(c2), 240), color=(0, 255, 0), thickness=2)# 绘出倾角线
cv2.imshow("GuideLine", image)

1.2 摄像头的图像获取

1
ret, frame = Watch.Video.read() #获取摄像头图像数据

1.3 相机资源的释放

思考:Python中不对窗口,或者使用Python语言的硬件资源不释放会怎么样?

1
Watch.Video.release()

2. 激光追踪相关

要想实现激光点的巡线,以及红绿激光的相互跟随,首先要做到单个激光点的精确识别与控制移动,这里我们使用了二维舵机云台,精度在能够完成基本要求之内。使用USB 1080P摄像头进行画面捕获。

激光点在图像中的亮度远远大于其他图像像素点,不难想到在HSV空间中对图像进行操作更容易识别到激光点。

2.1 激光点识别的图形预处理

通过调用v2.cvtColor()函数的方法,将图像颜色空间转换到 HSV中,然后通过设定阈值,进行图像的二值化(阈值调节建议采用滑块创建的方法,并且函数需要赋初值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 寻找激光点
def detect_lasers(image):
# imgae:用于处理的画布图像,frame:原图像,且是最终数据展示的图像
global load_process# 声明使用外部定义的 load_process

hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)# 转换颜色空间为HSV

# 定义红色激光点hsv色彩阈值范围,用作二值化
lower_laser = np.array([0, 100, 180])
upper_laser = np.array([179, 255, 255])

# 创建二值化图像
mask_laser = cv2.inRange(hsv, lower_laser, upper_laser)
# cv2.imshow("mask_laser1", mask_laser)

# 闭运算
kernel = np.ones((5, 5), np.uint8)
mask_laser = cv2.morphologyEx(mask_laser, cv2.MORPH_CLOSE, kernel)

2. 2 激光点的轮廓识别

调用cv2.findContours()函数,对封闭图形进行检测,并且绘制出激光点的图形识别轮廓

函数解释:

  1. mask_laser:这是输入的二值图像,也就是说,它应该是经过阈值处理或其他形式的图像处理后得到的黑白图像(通常是单通道的,只有 0 和 255 两个像素值)。
  2. cv2.RETR_EXTERNAL:这是轮廓检索模式(Contour Retrieval Mode),指定了轮廓的检索模式。RETR_EXTERNAL 表示只检索最外层的轮廓,忽略内部的轮廓。还有其他的检索模式,比如 RETR_LISTRETR_TREE 等,可以根据需要选择不同的模式。
  3. cv2.CHAIN_APPROX_SIMPLE:这是轮廓的逼近方法(Contour Approximation Method),指定了轮廓的表示方法。CHAIN_APPROX_SIMPLE 表示压缩水平、垂直和对
  4. 第一个返回值 contours_laser 是一个列表,其中每个元素是一个 numpy 数组,表示一个检测到的轮廓

cv2.minAreaRect()

函数解释:

  • contour:这是一个轮廓,通常是通过 findContours 函数找到的其中一个轮廓对象,它是一个包含轮廓点的 numpy 数组。
  • cv2.minAreaRect(contour):这个函数接收一个轮廓作为输入,并返回一个 RotatedRect 对象,它表示包围轮廓的最小旋转矩形框。

具体来说,返回的 RotatedRect 对象包含以下信息:

  • center:矩形框的中心点坐标 (cx, cy)
  • size:矩形框的宽度和高度 (width, height)
  • angle:矩形框相对于水平方向的旋转角度(逆时针为正)

获取矩形框角点:

  1. box = cv2.boxPoints(rect)
    • cv2.boxPoints 函数接收一个 RotatedRect 对象作为输入,并返回该旋转矩形框的四个顶点坐标。
    • 这些顶点坐标是浮点型数据,表示为一个包含四个点的 numpy 数组。
  2. box = np.int0(box)
    • 这一步将上一步得到的四个顶点坐标 box 转换为整数类型的 numpy 数组。
    • np.int0() 函数会将浮点数数组四舍五入为最接近的整数,并返回一个整数类型的 numpy 数组。
1
2
3
4
5
6
7
8
9
# 寻找轮廓
contours_laser, _ = cv2.findContours(mask_laser, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
laser_coords = None
for contour in contours_laser:
rect = cv2.minAreaRect(contour)# 获取最小矩形框
laser_coords = tuple(map(int, rect[0]))# 矩形框的中心坐标
box = cv2.boxPoints(rect)# 矩形框的四个角点
box = np.int0(box)
cv2.drawContours(frame, [box], 0, (0, 0, 0), 2)# 绘制矩形框(原图像中)

2.3激光点的颜色区分

imge.shape[]

  • image.shape:这是一个 numpy 数组属性,用于获取图像的尺寸和通道数信息。
  • image.shape[:2]:这是对 shape 属性进行切片操作,[:2] 表示取前两个元素,即图像的高度和宽度。

具体来说,如果 image 是一个图像的 numpy 数组,例如形状为 (height, width, channels),那么 image.shape[:2] 将返回一个包含图像高度和宽度的元组 (height, width)

这种操作通常用于获取图像的空间尺寸信息,以便在处理图像时进行尺寸相关的计算或操作

方圆坐标确定:

  1. 获取输入坐标:
    • x, y = coords:这行代码将变量 coords 解构为 xy,分别表示中心点的横纵坐标。
  2. 确定左上角坐标:
    • x_start = max(0, x - radius):计算方形区域的左上角横坐标。x - radius 表示以中心点 x 为基础,向左偏移半径 radius 的距离。max(0, ...) 确保左上角横坐标不会小于 0,即不会超出图像左边界。
    • y_start = max(0, y - radius):计算方形区域的左上角纵坐标。同理,y - radius 表示以中心点 y 为基础,向上偏移半径 radius 的距离。max(0, ...) 确保左上角纵坐标不会小于 0,即不会超出图像上边界。
  3. 确定右下角坐标:
    • x_end = min(width - 1, x + radius):计算方形区域的右下角横坐标。x + radius 表示以中心点 x 为基础,向右偏移半径 radius 的距离。min(width - 1, ...) 确保右下角横坐标不会超过图像的右边界。
    • y_end = min(height - 1, y + radius):计算方形区域的右下角纵坐标。同理,y + radius 表示以中心点 y 为基础,向下偏移半径 radius 的距离。min(height - 1, ...) 确保右下角纵坐标不会超过图像的下边界。

get_pixel_sum

这段代码用于从给定的区域中提取红色(R)和绿色(G)颜色通道的值。让我来解释一下这段代码的含义:

  1. roi[:, :, 2]
    • roi 应该是图像的一个区域,即感兴趣的区域(Region of Interest,ROI)。
    • : 是一个表示所有行或所有列的切片操作。
    • [ : , : , 2] 表示选取所有行的所有列的第三个颜色通道。在 BGR 颜色空间中,第三个通道对应的是红色通道,所以这行代码获取了 ROI 中所有的红色通道值。
  2. roi[:, :, 1]
    • 与上面的代码类似,这行代码选取了所有行的所有列的第二个颜色通道。在 BGR 颜色空间中,第二个通道对应的是绿色通道,所以这行代码获取了 ROI 中所有的绿色通道值。

在大多数图像处理库中(如 OpenCV),图像默认是以 BGR 顺序存储的,而不是 RGB。这意味着在获取颜色通道时需要使用 [ : , : , 2] 来获取红色通道,[ : , : , 1] 来获取绿色通道,[ : , : , 0] 来获取蓝色通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 激光点RGB值获取
def get_pixel_sum(image, coords):
# 获取图像宽度和高度
height, width = image.shape[:2]
radius = 3

# 确定方圆的左上角和右下角坐标
x, y = coords
x_start = max(0, x - radius)
y_start = max(0, y - radius)
x_end = min(width - 1, x + radius)
y_end = min(height - 1, y + radius)

# 提取方圆区域
roi = image[y_start:y_end, x_start:x_end]

# 计算 R 和 G 通道总值
r_channel = roi[:, :, 2]
g_channel = roi[:, :, 1]
r_sum = int(r_channel.sum())
g_sum = int(g_channel.sum())
return r_sum, g_sum

cv2.circle(frame, laser_coords, 4, (0, 0, 255), -1)

绘制实心圆

cv2.putText()

  1. 参数解释:
    • frame:这是目标图像帧,即要在其上添加文本的图像。
    • "RED":要写入的文本内容,这里是字符串 “RED”。
    • (laser_coords[0] - 10, laser_coords[1] - 10):文本放置的起始位置,由 laser_coords 提供,向左上方偏移了 (10, 10) 的像素值。
    • cv2.FONT_HERSHEY_SIMPLEX:字体类型,这里使用了简单的字体类型。
    • 0.5:字体大小因子,控制文本大小相对于基础字体的比例。
    • (0, 0, 255):文本的颜色,这里是红色,对应 BGR 颜色空间中的 (0, 0, 255)
    • 2:文本的线宽,即文本轮廓的粗细。
  2. 作用说明:
    • 这行代码的主要作用是在图像帧的指定位置绘制红色的文本 “RED”。通常用于在图像或视频中标记特定的对象、位置或状态信息。
  3. 注意事项:
    • 如果 laser_coords 提供的坐标 (x, y) 位于图像的边缘,确保 (x - 10, y - 10) 不会超出图像边界,以避免出现越界错误。
1
2
3
4
5
6
7
8
9
# 是否获取到激光点矩形框的中心坐标,防止读取值为空
if laser_coords is not None:
# 获取激光点的RGB数据
color_vel = get_pixel_sum(image, laser_coords)

# 在红色激光点中心坐标出绘制圆点图像(原图像中)
cv2.circle(frame, laser_coords, 4, (0, 0, 255), -1)
# 打印文本数据“RED”在红色激光点上
cv2.putText(frame ,"RED", (laser_coords[0] - 10, laser_coords[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)

2.4差值计算与串口通信

PS:上位机与下位机通信,建议采用十六进制:帧头+数据类型+数据内容 的单帧数据包格式,这样才能保证下位机能够同步反应上位机发送的特征值,减小通信的复杂度,同时可靠性和速率较高,不建议采用发送字符串类型的通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    if mode == 'A':
direction = long_path[load_process]
# 获取激光点与图像中心坐标或目标坐标的差值
x_err = laser_coords[0]-direction[0]
y_err = laser_coords[1]-direction[1]
elif mode == 'B':
x_err = laser_coords[0]-center_point_x
y_err = laser_coords[1]-center_point_y

# 如果激光点到达目标点(误差小于定值),目标点切换到下一个路径点
if abs(x_err) <= 5 and abs(y_err) <= 5:
load_process-=1

if load_process == -1:
load_process=11

# 串口发送,数据帧格式:0xFF(帧头),<x轴误差的数据类型>,<y轴误差的数据类型>,<x轴误差大小>,<y轴误差大小>
if x_err < 0:
x_data_type = 0x2D # -x
x_data = abs(x_err)
else:
x_data_type = 0x2B # +x
x_data = x_err
if y_err < 0:
y_data_type = 0x3D # -y
y_data = abs(y_err)
else:
y_data_type = 0x3B # +y
y_data = y_err

hex_data = bytes([0xFF,x_data_type,y_data_type,x_data, y_data])#y-

print(hex_data.hex())
ser.write(hex_data)

#print('x:'+ str(x_err))
#print('y:'+ str(y_err))
else:
print("没找到激光点")
return image
#cv2.circle(image, (center_x, center_y), 5, (0, 255, 0), -1)
#cv2.imshow("Laser Detection", image)

3.胶带矩形框识别

3.1 ROI区域的创建

PS: 使用ROI区域后,又想还原到原图像上对图像进行处理,就需要对坐标进行还原操作,保证图形每个像素点的索引和原图像对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ROIPixelProcessor:
def __init__(self):
pass

def process_roi(self, image, roi):
"""
取得 ROI 区域的像素值进行处理,但不改变索引号
:param image: 输入图像
:param roi: ROI 参数 (x, y, width, height)
:return: ROI 区域的像素值
"""
x, y, w, h = roi

# 确保 ROI 在图像范围内
if x < 0:
w += x
x = 0
if y < 0:
h += y
y = 0
if x + w > image.shape[1]:
w = image.shape[1] - x
if y + h > image.shape[0]:
h = image.shape[0] - y

# 提取 ROI 区域的像素值
roi_pixels = image[y:y+h, x:x+w]
return roi_pixels

3.2 图像的预处理

通过BGR转灰度,自设定阈值进行二值化地方法,将胶带部分转换为白色单值,其他部分全为黑色单值,并且通过滤波、腐蚀、闭运算地方法对图形进行处理,降低噪点干扰,提高图形识别完整性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if time.time()-start_time <= 5:       

# 将图像转换为灰度图
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
#cv2.imshow('gray',gray)

gray = cv2.GaussianBlur(gray, (9, 9), 0) #高斯平滑滤波,(9,9)是卷积核,0 代表函数自动计算标准差

#cv2.imshow('gauss',gray)
# 对灰度图进行阈值处理,将黑色部分变为白色,其他部分变为黑色
_, threshold = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)

# 进行形态学闭操作,填充内部空洞
kernel = np.ones((5,5),np.uint8)
closing = cv2.morphologyEx(threshold, cv2.MORPH_CLOSE, kernel, iterations=3)

# 进行形态学腐蚀操作,缩小黑框大小,减少噪点信号干扰
kernel = np.ones((3,3),np.uint8)
erosion = cv2.erode(closing,kernel,iterations = 1)
#dilation = cv2.dilate(threshold, kernel, iterations=1)

3.3 矩形框轮廓的识别

同样地,使用封闭图形检测算法,检测矩形框轮廓,这里使用 ROI处理,要特别注意像素点索引(坐标)的复原的操作。

即:

1
2
3
4
# 将轮廓绘制在原始图像上
for contour in contours:
# 将轮廓点的坐标映射回原始图像的坐标系
contour += (x, y) # 将ROI的偏移加回来

近似曲线

注意:这里approx_contour得到的是最终近似矩形的四个角点的坐标!

1
2
epsilon = 0.01 * cv2.arcLength(max_contour, True)
approx_contour = cv2.approxPolyDP(max_contour, epsilon, True)
  • cv2.arcLength 用于计算轮廓的周长或弧长。
  • epsilon 是一个近似精度参数,通常是轮廓周长的某个百分比,这里设定为总周长的1%。
  • cv2.approxPolyDP 用来对轮廓进行多边形逼近,通过减少顶点的数量来近似表示轮廓。
  • approx_contour 是近似后的多边形轮廓。

找到左右轮廓的边界点(列表)

leftmost[0]就是左轮廓第一个点

1
2
leftmost = min(path, key=lambda x: x[0])  # 左轮廓边界坐标点
rightmost = max(path, key=lambda x: x[0]) # 右轮廓边界坐标点
  • path 是一组坐标点的集合,通常表示为一个列表或数组。
  • min(path, key=lambda x: x[0])max(path, key=lambda x: x[0]) 分别用于找到 path 中横坐标 x 最小和最大的点,即左轮廓和右轮廓的边界点。

参数说明

  • lambda x: x[0] 是一个匿名函数,用于指定以每个点的横坐标 x 作为比较的关键字。因此,min 函数找到具有最小横坐标的点,而 max 函数找到具有最大横坐标的点。

计算轮廓的中心点

1
2
3
center_point_x = int(sum([point[0] for point in path]) / len(path))
center_point_y = int(sum([point[1] for point in path]) / len(path))
center_point = (center_point_x, center_point_y)
  • path 是一组轮廓的坐标点集合,通常表示为一个列表或数组。
  • sum([point[0] for point in path])path 中所有点的横坐标进行求和。
  • sum([point[1] for point in path])path 中所有点的纵坐标进行求和。
  • len(path)path 中点的数量,即轮廓的长度或大小。

参数说明

  • center_point_xcenter_point_y 分别是计算得到的轮廓中心点的横坐标和纵坐标。
  • center_point 是由 center_point_xcenter_point_y 组成的元组,表示轮廓的中心点坐标 (x, y)

作用

  • 这段代码的作用是计算轮廓 path 的几何中心点,通过对所有点的坐标进行平均值计算得出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#cv2.imshow('dilation',erosion)

roi = closing[center_y-120:center_y+120, center_x-120:center_x+120]#创建ROI图像

#cv2.imshow('roi',roi)
x,y,w,h = center_x-120,center_y-120,240,240 #ROI左上角点为220,140,ROI检测框高度为200,宽度为200

#绘制ROI矩形
cv2.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),1)

#cv2.imshow('erosion',erosion)
# 利用轮廓检测函数找到黑色框的轮廓
contours, hierarchy = cv2.findContours(roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 轮廓 层级 轮廓检索模式 轮廓逼近方法

# 将轮廓绘制在原始图像上
for contour in contours:
# 将轮廓点的坐标映射回原始图像的坐标系
contour += (x, y) # 将ROI的偏移加回来
#cv2.drawContours(frame, [contour], -1, (0, 0, 255), 2) 原始轮廓,这里用近似法得到更平滑的外围轮廓

# 如果找到了轮廓
if len(contours) > 0:
# 提取最大的轮廓
max_contour = max(contours, key=cv2.contourArea)

# 近似曲线
epsilon = 0.01 * cv2.arcLength(max_contour, True)
approx_contour = cv2.approxPolyDP(max_contour, epsilon, True)

# 获取路径坐标
path = []
for point in approx_contour:
path.append(tuple(point[0]))

# 找到左右轮廓的边界点
leftmost = min(path, key=lambda x: x[0])# 左轮廓边界坐标点
rightmost = max(path, key=lambda x: x[0])# 右轮廓边界坐标点

# 计算左右轮廓的中间点
middle_x = int((leftmost[0] + rightmost[0]) / 2)# x中心坐标
middle_y = int((leftmost[1] + rightmost[1]) / 2)# y中心坐标
middle_point = (middle_x, middle_y)

# 绘制左右轮廓中心点
#cv2.circle(frame, middle_point, 5, (0, 255, 0), -1)

# 计算轮廓的中心点
center_point_x = int(sum([point[0] for point in path]) / len(path))
center_point_y = int(sum([point[1] for point in path]) / len(path))
center_point = (center_point_x, center_point_y)

cv2.drawContours(frame, [np.array(path)], -1, (125, 255, 125), 1) #近似后的轮廓
# 轮廓 第几个(默认-1:所有) 颜色 线条厚度
# 绘制轮廓中心点
cv2.circle(frame, center_point, 3, (0, 255, 0), -1)

resize = 0.92

resize_path = []

for point in path:
resize_x = int(center_point_x + resize*(point[0]-center_point_x))
resize_y = int(center_point_y + resize*(point[1]-center_point_y))
resize_path.append((resize_x,resize_y))

cv2.drawContours(frame, [np.array(resize_path)], -1, (125, 255, 125), 1) # 内缩后的轮廓
long_path = interpolate_points(resize_path)

for point in long_path:
cv2.circle(frame, point, 4, (0, 0, 255), -1)

3-4 矩形框的路径规划-插值算法

如果让激光点沿着轮廓一个一个坐标点去走,这样计算量会相当大,不如将矩形轮廓通过近似得到四个角点,再通过插值法,设置相邻点的插值参数,得到12个路径点,这样就既保证了巡线的稳定性,也大大减少了巡线的计算量。同时,通过调用time库来计时,程序启动时定时5s后,这段时间提供给我们确认摄像机状态,移动ROI区域,完成路径规划,实现“动态建图”的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#插值算法
def interpolate_points(points):
interpolated_points = []
for i in range(len(points)):
start_point = points[i]
end_point = points[(i+1) % len(points)]
# 计算两点之间的距离
dx = end_point[0] - start_point[0]
dy = end_point[1] - start_point[1]
distance = 3
# 根据距离进行插值
for j in range(distance):
x = start_point[0] + int(dx * j / distance)
y = start_point[1] + int(dy * j / distance)
interpolated_points.append((x, y))
return interpolated_points

电控部分