【OpenCV+STM32】二维云台颜色识别及追踪

python 复制代码
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)  # 转换到 HSV 空间,便于颜色识别

实现目标:

  • Python + OpenCV 识别色块并追踪目标

  • 通过串口发送舵机控制指令给 STM32F103C8T6

  • STM32(标准库)通过定时器输出 PWM 控制两个舵机(180° + 360°)

目标分解:

1.Python + OpenCV识别到红色物体并把坐标通过串口通信发送给STM32,STM32把坐标进行回传判断是否正确接收

2.STM32控制两个舵机从0度到180度以相同速度转动

3.STM32控制舵机和通信相融合,能通过两个变量控制舵机转的角度,并把角度显示在OLED屏幕上

4.修改Python和STM32通信的消息,把原来图像中心坐标改为控制两个舵机转动的角度,由Python程序控制舵机转动的角度

5.修改Python程序,根据红色物体中心的坐标和图像的偏移量控制舵机转动的角度,调整映射关系

tips:每一步完成之后对文件进行备份,STM32的文件可备份成压缩包,python文件复制一份,都放进所建的工程文件中,防止之后修改时污染源文件

用电安全:

上电顺序,单片机,串口,舵机 (先对小功率进行供电)

断电顺序,舵机,串口,单片机 (先断掉大功率用电器)

烧录时DAP-lLINK出现了短路,故使用时DAP-lLINK应通过隔离器和电脑USB口相连

单片机接到面包板上时,舵机是5V供电(用可控电源),而OLED是3.3V供电,两部分应该共地

可控电源对舵机进行供电时,应在与舵机相连之前调到5V,断电连接,在上电;防止直接上电不确定电压烧毁舵机

单片机不能同时用Mico-USB和SWD下载端供电

代码部分:

Python部分

一.库导入

python 复制代码
import cv2          # OpenCV:用于摄像头采集、图像处理
import numpy as np  # NumPy:用于数组运算和数值处理
import serial       # pySerial:用于与 STM32 串口通信
import time         # time:用于延时,等待串口稳定

二.串口初始化

python 复制代码
port_name = "COM8"          # STM32 所连接的串口号(根据电脑实际情况修改)
baud_rate = 115200          # 串口通信波特率,需与 STM32 端一致,115200快且稳定最常用

try:
    ser = serial.Serial(    # 创建串口对象并尝试打开串口
        port_name,
        baud_rate,
        timeout=0.1         # 读取超时时间(非阻塞读取),读取串口数据最多等待 0.1 秒,超过时间返回空数据
    )
except serial.SerialException as e:  # 捕获串口打开异常,可能串口号不存在,串口被占用,没有权限,把这个错误保存到变量e中
    print(f"串口打开失败: {e}")
    exit()                  # 串口打开失败直接退出程序

time.sleep(2)               # 等待 STM32 复位和串口初始化完成,很多 STM32 板子串口打开 → 自动复位,复位期间不接收数据;系统需要时间初始化串口,立刻发数据容易丢
print(f"使用串口: {port_name}")  # 打印当前使用的串口

except,Python 的关键字,表示:捕获异常;serial.SerialException,这是 pySerial 库定义的一种异常类型,专门用来表示 串口相关错误;as e,把"异常对象"保存到变量 e,e 里面包含了 错误的详细信息

exit(),它是 Python 提供的一个函数,作用:终止当前程序运行

三.摄像头初始化

python 复制代码
cap = cv2.VideoCapture(0)   # 打开默认摄像头(0 表示第一个摄像头)

cv2.VideoCapture,OpenCV 提供的 视频输入接口,作用连接摄像头,或打开视频文件,并提供 read() 方法读取帧;0表示 第 0 个摄像头,一般是:笔记本内置摄像头,或第一个 USB 摄像头;cap 是一个对象,里面保存了:摄像头连接状态,缓冲区,视频参数(分辨率、帧率等)

四.舵机角度初始化

python 复制代码
servo1_angle = 90           # 舵机1初始角度(Y 方向,居中)
servo2_angle = 90           # 舵机2初始角度(X 方向,居中)

五.STM32通信函数

1️⃣发送舵机角度到STM32

python 复制代码
def send_to_stm32(servo1_angle, servo2_angle):
    # 按约定格式组织串口数据,末尾加换行符
    message = f"S1{servo1_angle},S2{servo2_angle}\n"  #以 换行符作为一条指令结束标志,如果不加,STM32 不知道什么时候一条命令结束,会解析失败或卡住,串口通信最重要的就是"分帧"

    try:
        ser.write(message.encode())  # 将字符串编码后写入串口,message 是字符串(str),串口只能发 字节(bytes),.encode()把字符串转成字节流(默认 UTF-8)
        ser.flush()                  # 强制立即发送缓冲区数据,舵机控制不能延迟
        print(f"发送到 STM32: {message.strip()}")  # 打印发送内容,.strip()去掉 \n,打印更干净
    except serial.SerialException as e:
        print(f"串口写入失败: {e}")  # 捕获串口写入异常

message = f"S1{servo1_angle},S2{servo2_angle}\n",f (format)表示这是一个"格式化字符串(f-string)",告诉 Python:

"这个字符串里有变量,要先计算再生成字符串"

可以把变量的值直接"塞进"字符串里,{} 里的变量会被实际数值替换

{servo1_angle}表示:取变量 servo1_angle 的当前值,转成字符串,拼接到字符串中

2️⃣读取STM32回传的数据

python 复制代码
def read_from_stm32():
    try:
        while ser.in_waiting:        # 当串口接收缓冲区有数据时
            line = ser.readline()    # 读取一行数据(以 \n 结尾)
            line = line.decode('utf-8', errors='ignore').strip()  # 解码并去除换行
            if line:                 # 如果不是空行,判断一下,防止打印空行
                print(f"STM32 回传: {line}")  # 打印 STM32 返回信息
    except Exception as e:
        print(f"串口读取异常: {e}")  # 捕获读取异常

为什么要 decode,(line的类型是:bytes)串口收到的是 字节(bytes), Python 打印和处理需要 字符串(str), decode:字节 → 字符串;UTF-8 是最常用的字符编码;errors='ignore'如果遇到非法字符,直接跳过;.strip()去掉字符串两端的\n (换行,光标移动到下一行 ),\r(回车,光标回到行首 )(很多终端只有在收到 \r\n 时,才会正确"回到行首并换行),空格

line = line.decode('utf-8', errors='ignore').strip() 把从串口读到的"原始字节数据",转换成"干净、可打印、可比较的字符串"。

六.主循环

python 复制代码
try:
    while True:  # 程序主循环,持续运行直到手动退出

七.图像采集与基本信息

python 复制代码
        ret, frame = cap.read()  # 从摄像头读取一帧图像
        if not ret:              # 如果读取失败
            break                # 退出循环

        height, width = frame.shape[:2]  # 获取图像高度和宽度
        center_x = width // 2             # 图像中心 X 坐标
        center_y = height // 2            # 图像中心 Y 坐标

cap.read() :从摄像头(由 cv2.VideoCapture 创建的对象 cap)中读取一帧图像。

返回值:ret :布尔值,表示读取是否成功,如果成功读取一帧图像,retTrue;如果读取失败(例如摄像头断开或图像读取错误),retFalse

if not ret: :如果读取失败(即 retFalse),则执行下方代码。

frame.shape :返回一个元组,包含图像的形状((height, width, channels)),即图像的高度、宽度和通道数。height:图像的高度(即图像的行数),width:图像的宽度(即图像的列数),channels:图像的通道数,通常为 3,表示彩色图像的 RGB 通道。frame.shape[:2] :切片操作,取 frame.shape 元组(不可变的序列,支持切片和访问)的前两个值(heightwidth),并将其分别赋给 heightwidth 变量。

center_x = width // 2 :计算图像的水平中心坐标,// 是整除运算符,确保返回一个整数值。

八.颜色空间转换

python 复制代码
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)  # 转换到 HSV 空间,便于颜色识别

把摄像头拍到的彩色图像,从 BGR 颜色空间转换成 HSV 颜色空间,目的是:更稳定、更容易地做颜色识别(比如红色)。OpenCV 读到的彩色图像是BGR,HSV 把颜色拆成 3 个独立概念:色相(颜色)、饱和度(纯度)、亮度

九.红色阈值定义

python 复制代码
        lower_red1 = np.array([0, 120, 70])    # 红色低区间(Hue 低端)
        upper_red1 = np.array([10, 255, 255])

        lower_red2 = np.array([170, 120, 70])  # 红色高区间(Hue 高端)
        upper_red2 = np.array([180, 255, 255])

定义"红色"在 HSV 颜色空间中的取值范围,并且由于 红色在 HSV 里是"断开的",所以要分成 两段。HSV 里的 Hue(色相)是一个"圆",OpenCV 中:H ∈ [0, 180],0 和 180 是连在一起的(像圆环),红色刚好在 圆环的起点和终点(靠近 0 的一段,靠近 180 的一段)。每个数组里 3 个数分别是[ H, S, V ]

十.红色掩膜生成

python 复制代码
        mask1 = cv2.inRange(hsv, lower_red1, upper_red1)  # 提取低区间红色
        mask2 = cv2.inRange(hsv, lower_red2, upper_red2)  # 提取高区间红色
        mask = cv2.bitwise_or(mask1, mask2)               # 合并两个掩膜

从 HSV 图像中,把"红色的像素"全部挑出来,生成一张只有黑白的二值图(掩膜)。

白色(255):红色像素 黑色(0):非红色像素

后面找轮廓、算中心点,用的就是掩膜,而不是原图。

mask1 = cv2.inRange(hsv, lower_red1, upper_red1) ,如果 lower_red1 ≤ 像素(H,S,V) ≤ upper_red1 ,mask1 中该像素 = 255(白);否则,mask1 中该像素 = 0(黑)

mask = cv2.bitwise_or(mask1, mask2),把 mask1 和 mask2 合成一张总的红色掩膜

十一.去噪与形态学处理

python 复制代码
        kernel = np.ones((5, 5), np.uint8)  # 定义 5x5 卷积核

        mask = cv2.morphologyEx(            # 开运算:去除小噪点
            mask,
            cv2.MORPH_OPEN,
            kernel
        )

        mask = cv2.morphologyEx(            # 膨胀:增强目标区域
            mask,
            cv2.MORPH_DILATE,
            kernel
        )

让红色掩膜从"脏乱的点"变成"干净的一整块"。

kernel = np.ones((5, 5), np.uint8)

(5, 5)表示5行5列;np.uint8表示数组元素的数据类型是:无符号 8 位整数,取值范围:0 ~ 255,图像像素本身就是 uint8

1 1 1 1 1 用一个 5×5 的窗口,在图像上"滑动"做判断

1 1 1 1 1 开运算 = 先腐蚀(Erode)→ 再膨胀(Dilate)

1 1 1 1 1 腐蚀:只有当 kernel 覆盖范围内全是白色,中心像素才保留为白色,否则 → 变黑

1 1 1 1 1 膨胀:kernel 范围内只要有一个白点,中心像素就变白

1 1 1 1 1

为什么开运算后还要再膨胀?因为开运算的"腐蚀"步骤会让目标变小,再膨胀一次,把目标补回来,甚至稍微放大(更稳定)

十二、轮廓查找

python 复制代码
        contours, _ = cv2.findContours( # 查找二值图像中的轮廓
            mask,              #单通道二值图像
            cv2.RETR_TREE,     #轮廓检索模式,找所有轮廓,保留层级
            cv2.CHAIN_APPROX_SIMPLE  #轮廓点的存储方式,直线段只存起点和终点,只保留关键拐点
        )

在二值图像中,把所有"白色区域的边界"找出来,并用点的形式表示。白色区域 = 目标,轮廓 = 目标的外边缘

十三、目标筛选与中心计算

python 复制代码
        if contours:                         # 如果检测到轮廓
            largest_contour = max(           # 选取面积最大的轮廓
                contours,
                key=cv2.contourArea
            )

            if cv2.contourArea(largest_contour) > 500:  # 过滤小噪声,如果最大轮廓面积仍然很小,那说明画面里没有真正目标,只是噪声
                x, y, w, h = cv2.boundingRect(largest_contour)  # 获取外接矩形

                obj_center_x = x + w // 2    # 目标中心 X 坐标
                obj_center_y = y + h // 2    # 目标中心 Y 坐标

从一堆轮廓里,确定你真正要跟踪的那个目标

十四、可视化标记

python 复制代码
                cv2.rectangle(              # 绘制目标矩形框
                    frame,   #视频画面本身
                    (x, y),   #矩形左上角坐标
                    (x + w, y + h),
                    (0, 255, 0), #矩形颜色(BGR格式) 绿色
                    2  #线条粗细(像素),2 像素既清晰又不挡画面
                )

                cv2.circle(                 # 绘制目标中心点
                    frame,
                    (obj_center_x, obj_center_y),  #圆心坐标
                    5,   #圆的半径(像素)
                    (255, 0, 0),   #颜色:蓝色(BGR)
                    -1   #填充方式,-1 表示 实心圆
                )

                cv2.putText(                # 显示文字标签
                    frame,
                    'Red Object',  #显示的字符串,用来标识目标类别
                    (x, y - 10),  #文字起始位置(左下角)
                    cv2.FONT_HERSHEY_SIMPLEX,  #字体类型,OpenCV 自带字体,清晰、通用
                    0.6,  #字体缩放比例
                    (0, 255, 0),  #文字颜色,和矩形颜色一致,视觉统一
                    2
                )

在原始视频画面上"画出你检测到的目标",让你能直观看到:目标位置、目标大小、目标中心、当前识别结果是否正确

十五、偏差计算(像素级)

python 复制代码
                diff_x = obj_center_x - center_x  # X 方向偏差(右为正)
                diff_y = obj_center_y - center_y  # Y 方向偏差(下为正)

                print(
                    f"Red Object Center: ({obj_center_x}, {obj_center_y}) "
                    f"| Diff: (dx={diff_x}, dy={diff_y})"
                ) #在运行时会被 实时替换成变量当前的值
#Python 的 f-string(格式化字符串),Python 会先分别格式化,再拼接成一个字符串。

计算目标中心相对于画面中心的"位置误差",并打印出来用于调试和控制。例如 :

十六、舵机角度映射

python 复制代码
                servo2_angle = int(          # X 方向舵机角度计算
                    np.clip(
                        90 + diff_x // 10,   # 偏差 → 角度增量,90° = 云台"正中间"位置
                        0,
                        180   #舵机物理角度范围:0°~180°
                    )
                )

                servo1_angle = int(          # Y 方向舵机角度计算
                    np.clip(
                        90 - diff_y // 10,   # 图像坐标与舵机方向相反,OpenCV 坐标系:Y 向下为正
                        0,
                        180
                    )
                )
现象 调哪里
云台太慢 改 10 → 5,同样误差 → 舵机动得更多
云台抖 改 10 → 15,让舵机动作更小、更慢、更稳,所以抖动会减轻,小误差被"忽略"
上下反了 -,翻转控制方向
左右反了 + / -,物理系统没有"标准方向",软件必须迁就硬件
撞限位 改 clip 范围,实际舵机:可能只能转 20°~160°,可能只能转 20°~160°,程序以为是 0~180
diff //10 //15
15
10
diff //10 //5
50 10°
20

十七、发送控制 + 接收反馈

python 复制代码
        send_to_stm32(servo1_angle, servo2_angle)  # 向 STM32 发送舵机角度
        read_from_stm32()                           # 读取 STM32 回传数据

十八、图像显示与退出控制

python 复制代码
        cv2.imshow('Red Object Tracking', frame)  # 显示处理后的视频画面,第一个参数窗口名,第二个参数要显示的图像

        if cv2.waitKey(1) & 0xFF == ord('q'):      # 按 q 键退出
            break
waitKey 效果
0 一直等,视频卡死
1 实时刷新
30 限制帧率(约 30 FPS)

waitKey 返回的是一个 32 位整数, 不同平台高位可能有垃圾数据,& 0xFF只取 低 8 位, 保证兼容性,ord('q')把字符 'q' 转成 ASCII 码,'q' → 113

十九、资源释放

python 复制代码
finally:
    cap.release()            # 释放摄像头资源
    cv2.destroyAllWindows()  # 关闭所有 OpenCV 窗口
    ser.close()              # 关闭串口
相关推荐
nassi_3 小时前
ESP8266 Wi-Fi模块解析
stm32·嵌入式硬件
向阳逐梦3 小时前
马达驱动芯片核心逻辑:从信号到动力的“功率放大密码”
单片机·嵌入式硬件
1+2单片机电子设计3 小时前
基于 STM32 的羽毛球运动状态监测系统设计
stm32·单片机·嵌入式硬件
国科安芯3 小时前
CANFD 总线多节点扩展技术:节点数量限制与突破方案
单片机·嵌入式硬件·安全性测试
电子小子洋酱3 小时前
Linux驱动开发学习笔记(更新中)
linux·笔记·单片机
走错路的程序员5 小时前
C语言单片机与C#上位机之间传递大量参数比较好的实践方案
c语言·单片机·c#
猪八戒1.06 小时前
【梅花】2.工程模板的搭建
单片机·嵌入式硬件
清风6666666 小时前
基于单片机的井盖安全监测与报警上位机监测系统设计
单片机·嵌入式硬件·安全·毕业设计·课程设计·期末大作业
清风6666666 小时前
基于单片机的多功能LCD音乐播放器设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业