PythonAI算法
2025 电赛 E 题视觉追踪系统开源实现
本文介绍了 2025 年全国大学生电子设计大赛 E 题的视觉追踪系统开源实现。系统采用 MicroPython 编写,基于摄像头进行 A4 纸目标识别,结合 PID 算法控制双轴舵机完成固定位置追踪和动态搜索。代码涵盖硬件初始化、串口通信、图像处理和主任务逻辑,具备激光锁定及输出平滑功能,为嵌入式视觉竞赛提供完整参考方案。

本文介绍了 2025 年全国大学生电子设计大赛 E 题的视觉追踪系统开源实现。系统采用 MicroPython 编写,基于摄像头进行 A4 纸目标识别,结合 PID 算法控制双轴舵机完成固定位置追踪和动态搜索。代码涵盖硬件初始化、串口通信、图像处理和主任务逻辑,具备激光锁定及输出平滑功能,为嵌入式视觉竞赛提供完整参考方案。

# 依赖部分
import time
import os
import struct
from media.sensor import *
from media.display import *
from media.media import *
from time import ticks_ms
from machine import FPIOA, Pin, PWM, Timer
from machine import UART
# 全局变量配置
# 平滑处理变量
last_output_x = None
last_output_y = None
# X 轴上次输出值
# Y 轴上次输出值
# 输出变化率限制
max_change_x = 15 # X 轴最大单次变化量(度)
max_change_y = 5 # Y 轴最大单次变化量(度)
# 初始中心点坐标(屏幕中心)
last_center_x = 160
last_center_y = 120
# 目标点坐标(A4 纸中心)
target_x = 171
target_y = 118
# 显示和摄像头参数
DISPLAY_WIDTH = 310
DISPLAY_HEIGHT = 277
# A4 纸检测阈值
black_threshold = (19, 100, -73, 77, -61, 63)
white_threshold = 330
# 传感器声明
sensor = None
# PID 参数(比例、积分、微分)
Kp = 0.5
Ki = 0.04
Kd = 0.2
# PID 控制变量
last_error_x = 0
last_error_y = 0
integral_x = 0
integral_y = 0
# 舵机控制量
servo_dx = 0
servo_dy = 0
# 引脚分配
fpioa = FPIOA()
fpioa.set_function(34, FPIOA.GPIO34)
fpioa.set_function(35, FPIOA.GPIO35)
fpioa.set_function(40, FPIOA.UART1_TXD)
fpioa.set_function(41, FPIOA.UART1_RXD)
fpioa.set_function(60, FPIOA.GPIO60)
# 硬件接口初始化
key0 = Pin(34, Pin.IN, pull=Pin.PULL_UP, drive=7)
key1 = Pin(35, Pin.IN, pull=Pin.PULL_UP, drive=7)
lazer = Pin(60, Pin.OUT, pull=Pin.PULL_UP, drive=7)
uart1 = UART(UART.UART1, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
def servo_control(servo_id, angle, interval=50):
""" 多圈角度控制
功能:控制舵机旋转到指定角度
输入:servo_id(0 上/1 下), angle(度), interval(ms)
"""
angle_raw = int(angle * 10) # 角度值放大 10 倍
# 构建数据包
packet = bytearray([0x12, 0x4C, 0x0D, 0x0B, servo_id])
packet.extend(struct.pack('<i', angle_raw))
packet.extend(struct.pack('<I', interval))
packet.extend(struct.pack('<H', 0)) # power=0
packet.append(sum(packet) % 256) # 校验和
uart1.write(bytes(packet)) # 发送指令
def servo_read(servo_id):
""" 读取多圈角度
功能:读取舵机当前角度和圈数
输入:servo_id(0 上/1 下)
"""
packet = bytearray([0x12, 0x4C, 0x10, 0x01, servo_id])
packet.append(sum(packet) % 256) # 校验和
uart1.write(bytes(packet)) # 发送指令
def parse_response(data):
""" 解析角度响应
功能:解析舵机返回的角度数据
输入:data 字节数据
输出:(servo_id, angle, turns)或 None
"""
if len(data) >= 12 and data[0] == 0x05 data[] == :
servo_id = data[]
angle = struct.unpack(, data[:])[] /
turns = struct.unpack(, data[:])[]
(servo_id, angle, turns)
():
data = uart1.read()
data:
result = parse_response(data)
result:
servo_id, angle, turns = result
()
result
time.sleep_ms()
def pid_control(error, last_error, integral, axis='x'):
""" PID 控制函数
功能:计算 PID 输出值
输入:error(当前误差), last_error(上次误差), integral(积分项), axis(控制轴)
输出:(output, last_error, integral)
"""
# 根据控制轴选择参数
if axis == 'x':
# X 轴参数配置
if abs(error) >= 0: # 大误差:快速响应
Kp, Ki, Kd, sep_thresh = 0.6, 0.2, 1, 10000000
else: # 小误差:精确控制
Kp, Ki, Kd, sep_thresh = 0.5, 0.3, 0.2, 5000
else: # Y 轴
if abs(error) >= 0: # 大误差
Kp, Ki, Kd, sep_thresh = 0.6, 0.2, 2, 8000
else: # 小误差
Kp, Ki, Kd, sep_thresh = 0.6, 0.15, 0.2, 60
# PID 计算
Proportional = Kp * error # 比例项
# 积分分离:大误差时不累积积分
if abs(error) < sep_thresh:
integral += error
integral_term = Ki * integral # 积分项
derivative = Kd * (error - last_error) # 微分项
output = Proportional + integral_term + derivative
last_error = error
return output, last_error, integral
():
last_output :
new_output
change = new_output - last_output
(change) > max_change:
change > :
smoothed_output = last_output + max_change
:
smoothed_output = last_output - max_change
:
smoothed_output = new_output
smoothed_output
def init_sensor():
"""初始化摄像头"""
global sensor
print("Starting camera tracking system...")
sensor = Sensor(width=1920, height=1080)
sensor.reset()
sensor.set_framesize(Sensor.QVGA) # 320*240
sensor.set_pixformat(Sensor.RGB565)
def init_display():
"""初始化显示屏"""
Display.init(Display.ST7701, to_ide=True, fps=60)
MediaManager.init() # 初始化媒体管理器
def find_a4_paper(img):
""" 寻找 A4 纸
功能:检测 A4 纸的黑色边框和白色区域
输入:img(图像对象)
输出:(outer, inner) - 外框和内框坐标
"""
# 1. 检测黑色边框
black_blobs = img.find_blobs([black_threshold], area_threshold=1, merge=True, margin=1, invert=True)
for blob in black_blobs:
x, y, w, h = blob.rect()
# 跳过太小的区域
if w < 1 or h < 1:
continue
# 2. 在黑色边框内检测白色区域
inner_w = int(w * 0.9) # 假设白色区域比边框小 10%
inner_h = int(h * 0.9)
inner_x = x + (w - inner_w) // 2
inner_y = y + (h - inner_h) // 2
# 检查白色区域
white_pixels = 0
total_pixels = 0
# 采样内部区域中心部分
for i (inner_x + , inner_x + inner_w - , ):
j (inner_y + , inner_y + inner_h - , ):
pixel = img.get_pixel(i, j)
pixel[] + pixel[] + pixel[] > white_threshold:
white_pixels +=
total_pixels +=
total_pixels > white_pixels / total_pixels > :
(x, y, w, h), (inner_x, inner_y, inner_w, inner_h)
,
():
img:
outer, inner = find_a4_paper(img)
outer:
img.draw_rectangle(outer[], outer[], outer[], outer[], color=(, , ), thickness=)
inner:
img.draw_rectangle(inner[], inner[], inner[], inner[], color=(, , ), thickness=)
center_x = outer[] + outer[] //
center_y = outer[] + outer[] //
img.draw_cross(center_x, center_y, color=(, , ), size=)
Display.show_image(img, x=(( - sensor.width()) / ), y=(( - sensor.height()) / ))
center_x, center_y
:
()
Display.show_image(img, x=(( - sensor.width()) / ), y=(( - sensor.height()) / ))
,
:
()
Display.show_image(img, x=(( - sensor.width()) / ), y=(( - sensor.height()) / ))
,
:
()
,
def main_task2():
"""任务 2:固定位置追踪"""
try:
# 初始化硬件
init_sensor()
time.sleep_ms(200)
init_display()
sensor.run()
clock = time.clock()
# 全局变量声明
global last_error_x, last_error_y, integral_x, integral_y
global last_output_x, last_output_y
# 状态变量
valid_count_fps = 0
valid_count_lazer = 0
# 摆正逻辑
servo_control(0, 73.5) # Y 轴位置
servo_control(1, 0) # X 轴位置
while True:
clock.tick()
img = sensor.snapshot()
center_x, center_y = img_process(img)
if center_x is not None:
valid_count_fps += 1
if valid_count_fps >= 3: # 连续 3 帧检测到目标
# 计算偏差
error_x = target_x - center_x
error_y = target_y - center_y
# 死区处理
if abs(error_x) < 5:
error_x = 0
if abs(error_y) < 5:
error_y = 0
# 激光锁定条件
if abs(error_y) < 5 (error_x) < :
valid_count_lazer +=
valid_count_lazer >= :
lazer.value()
output_x, last_error_x, integral_x = pid_control(
error_x, last_error_x, integral_x, )
output_y, last_error_y, integral_y = pid_control(
error_y, last_error_y, integral_y, )
output_x = output_x *
output_y = output_y *
base_angle_y =
raw_output_y = base_angle_y + output_y
raw_output_y = (, (, raw_output_y))
raw_output_y = - raw_output_y
raw_output_x = (-, (, output_x))
raw_output_x = -raw_output_x
smooth_output_x = smooth_output(raw_output_x, last_output_x, max_change_x)
smooth_output_y = smooth_output(raw_output_y, last_output_y, max_change_y)
last_output_x = smooth_output_x
last_output_y = smooth_output_y
servo_control(, smooth_output_x)
servo_control(, smooth_output_y)
(.(raw_output_x, raw_output_y))
(.(smooth_output_x, smooth_output_y))
(.(integral_x, integral_y))
(.(Kp * error_x, Kp * error_y))
(.(Kd * error_x, Kd * error_y))
(clock.fps())
:
valid_count_fps =
KeyboardInterrupt:
()
Exception e:
()
:
() (sensor, Sensor):
sensor.stop()
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms()
lazer.value()
MediaManager.deinit()
()
def main_task3_right():
"""任务 3:动态搜索追踪(右转版)"""
try:
# 初始化硬件
init_sensor()
time.sleep_ms(200)
init_display()
sensor.run()
clock = time.clock()
# 全局变量声明
global last_error_x, last_error_y, integral_x, integral_y
global last_output_x, last_output_y
# 状态变量
valid_count_fps = 0
valid_count_lazer = 0
search_error_x = -100 # 搜索方向参数(右转)
base_angle_x = 0 # X 轴基准角度
while True:
clock.tick()
img = sensor.snapshot()
center_x, center_y = img_process(img)
if center_x is not None:
# 检测到目标
valid_count_fps += 1
if valid_count_fps >= 3:
# 计算偏差
error_x = target_x - center_x
error_y = target_y - center_y
# 死区处理
if abs(error_x) < 5:
error_x = 0
if abs(error_y) < 5:
error_y = 0
# 激光锁定条件
if abs(error_y) < 5 and abs(error_x) < :
valid_count_lazer +=
valid_count_lazer >= :
lazer.value()
output_x, last_error_x, integral_x = pid_control(
error_x, last_error_x, integral_x, )
output_y, last_error_y, integral_y = pid_control(
error_y, last_error_y, integral_y, )
output_x = output_x *
output_y = output_y *
base_angle_y =
raw_output_y = base_angle_y + output_y
raw_output_y = (, (, raw_output_y))
raw_output_y = - raw_output_y
raw_output_x = base_angle_x + output_x
raw_output_x = (-, (, raw_output_x))
raw_output_x = -raw_output_x
smooth_output_x = smooth_output(raw_output_x, last_output_x, max_change_x)
smooth_output_y = smooth_output(raw_output_y, last_output_y, max_change_y)
last_output_x = smooth_output_x
last_output_y = smooth_output_y
servo_control(, smooth_output_x)
servo_control(, smooth_output_y)
(.(raw_output_x, raw_output_y))
(.(smooth_output_x, smooth_output_y))
(.(integral_x, integral_y))
(.(Kp * error_x, Kp * error_y))
(.(Kd * error_x, Kd * error_y))
(clock.fps())
:
valid_count_fps =
valid_count_lazer =
output_x, last_error_x, integral_x = pid_control(
search_error_x, last_error_x, integral_x, )
output_x = output_x *
raw_output_x = base_angle_x + output_x
raw_output_x = (-, (, raw_output_x))
raw_output_x = -raw_output_x
raw_output_y =
raw_output_y = (, (, raw_output_y))
raw_output_y = - raw_output_y
smooth_output_x = smooth_output(raw_output_x, last_output_x, max_change_x)
smooth_output_y = smooth_output(raw_output_y, last_output_y, max_change_y)
last_output_x = smooth_output_x
last_output_y = smooth_output_y
servo_control(, smooth_output_x)
servo_control(, smooth_output_y)
()
KeyboardInterrupt:
()
Exception e:
()
:
() (sensor, Sensor):
sensor.stop()
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms()
lazer.value()
MediaManager.deinit()
()
# 程序入口
if __name__ == "__main__":
while True:
print("程序开始,等待按钮")
if key0.value() == 0: # 检测按键按下
time.sleep_ms(20)
if key0.value() == 0: # 确认按键按下
print("已检测到按下按钮")
lazer.value(1) # 开启激光
main_task2() # 启动任务 2
else:
time.sleep_ms(100) # 等待按键

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online