一、开发环境配置清单表格

类别 具体内容
操作系统 Windows
外设 摄像头版本:免驱UVC高清USB工业摄像头(720P-2.8MM)
开发工具 Python 解释器 + PyCharm IDE
Python 依赖包 - 科学计算:numpy 2.1.2 图像处理:opencv-python 4.13.0.92

二、项目背景

承接上文基于opencv的颜色检测,我们在上面的基础上添加功能。

1、视频功能,能实时检测我们的图像

2、轮廓检测,能大致的勾勒出目标颜色的范围。

三、项目能力提升点

练习本项目带来的能力上的提升:

        1、巩固上一项目,熟悉使用轨道条进行选定颜色,并进行掩码操作。

        2、学习使用 OpenCV 调整摄像头基本参数(亮度、分辨率)

        3、如何使用形态学操作去除噪点,使掩膜更干净

        4、轮廓检测去识别颜色的大致范围

四、核心代码逻辑

1、借助颜色识别实时选择你需要检测颜色并记录下目标颜色的HSV的极值

2、利用形态学降噪

3、添加轮廓检测功能,代码功能更加饱满

五、分步实现与关键说明

第一步:实时选择检测颜色并记录HSV的相应值

关键点一(必看)

事先说明,针对红色的HSV环绕特性,[0-10]和[170-179]都是红色的范围,但是这里我的参数的选取仅有[170-179]的是具有一定的局限性的。

这里面举一个我实际操作时遇到的问题,调了三次红色参数的,他们的结果如下:
 

你要说他们有什么区别吗?除了第一组参数,他的噪声点相比之下多了一些,我将它舍弃了以外,二和三的效果都感觉不错。而我最后选择的是第三组参数,因为他的轮廓貌似更准确一些,但是我最终项目呈现的代码选择的参数也不是第三组,而是还进行了进一步调参。

我想说,误差是无法避免的。我们无法找到最正确的参数,它的检测效果会受到,摄像机角度,目标物体的角度,外界亮度,与摄像机的远近、摄像机的焦距、甚至摄像机的种类等各种外界因素,所以我们只能找到局部最优解。甚至不是最优解,只是较优解,所以我建议你一定要多调参,调到厌烦为止,调的越多,就越能适合当时的环境。

当然,这里我也有自己的一些想法。大二曾接触过一部分的机器学习(仅仅只是学习完吴恩达的机器学习还稍微做了几个小项目(kaggle)),那我就在想,是否可以通过机器学习(梯度下降->自动调参)/深度学习(神经网络)进行调参,进一步优化。

关键点二

摄像头设置:

frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(1)
cap.set(3, frameWidth)
cap.set(4, frameHeight)
cap.set(10, 150)   # 亮度

这里需要注意的下面这条语句中的参数要根据你的外设去设置,按照我的外设是参数1,但如果你要使用电脑自带的摄像头参数可能就是0,一切根据自己的外设去调整。还有这里想要提醒你,如果是自己购置的摄像头,可能需要等待一会才会显示图像。

cap = cv2.VideoCapture(1)
关键点三

关闭窗口

# ===================== 修复代码:检测窗口是否关闭 =====================
    # 如果TrackBar窗口被关闭,直接退出循环
    if cv2.getWindowProperty("TrackBar", cv2.WND_PROP_VISIBLE) < 1:
        break
    # ====================================================================    
# 按 'q' 退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

这里提醒你,如果没有上述的修复代码,在关闭窗口要使用'q'键,尽量不要直接叉掉Trackbar的窗口,如果叉掉了,在代码结束以后,可能会出现报错。当然加入修复代码后,叉掉也是可以的。

第二步:使用形态学降噪

形态学降噪的核心是使用结构元素(SE)做腐蚀、膨胀及组合运算,剔除小于SE的噪声,保留。

降噪的方法有很多。在本项目中使用的是组合降噪的方法,先进行开运算以后在进行闭运算。

基本概念:

结构元素(SE),根据噪声大小定义一个小区间,这个区间会完成膨胀、腐蚀等操作。

在形状上有:矩形、椭圆形、十字

大小上:大多数是3×3,但具体看噪声大小,也有5×5和7×7可选。

腐蚀:收缩亮区域,消除小的亮噪声

膨胀:扩张亮区域,消除小的暗噪声

开运算(先腐蚀后膨胀):去除亮噪声、毛刺,适用于处理盐噪声;

闭运算(先膨胀后腐蚀):填补暗洞、修复断裂,适用于处理椒噪声。

代码实现
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)  # 先腐蚀后膨胀,去除小噪点
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 先膨胀后腐蚀,填补小空

未经噪声处理的图像:

经噪声处理的图像:

看起来貌似更加准确了一些。

我的理解

依旧是上一篇文章的橙色的一个小模块

0 1 2 3 4
0 (34,210,56) (78,45,210) (56,34,200) (89,123,45) (67,200,34)
1 (210,56,78) (255,165,0) (255,165,0) (45,78,123) (123,45,200)
2 (45,200,34) (255,165,0) (255,165,0) (200,34,78) (78,210,56)
3 (89,34,200) (56,200,89) (34,123,78) (210,56,45) (45,89,210)
4 (123,78,45) (200,67,89) (34,210,200) (56,34,123) (78,45,210)

下面的表格是对二值掩膜的简化模拟,白色代表检测到的颜色区域,黑色代表背景

0 1 2 3 4
0 (0,0,0) (0,0,0) (0,0,0) (0,0,0) (0,0,0)
1 (0,0,0) (255,165,0) (255,165,0) (0,0,0) (0,0,0)
2 (0,0,0) (255,165,0) (255,165,0) (0,0,0) (0,0,0)
3 (0,0,0) (0,0,0) (0,0,0) (0,0,0) (0,0,0)
4 (0,0,0) (0,0,0) (0,0,0) (0,0,0) (0,0,0)

如果是这样的像素点,应该会变成这样,也就是只有中间是橙色的,其他的都成了噪声点,全被处理了。但是实际处理的时候不会有那么理想,可能在这个目标区域附近还会有一些噪声是无法处理的。

第三步:轮廓检测丰富功能

概念很简单,在本项目中就是实现把相同颜色的区域给框选出来,看起来更清晰明了。当然再本项目中,轮廓检测并不复杂,简单了解

总的来说就是以下步骤:从二值遮罩 mask 中提取有效物体 → 过滤小噪点 → 找到每个物体的轮廓、外接矩形、中心点 → 在原图上画出轮廓、矩形、中心点。

代码实现
# ---------- 轮廓检测 ----------
    # 查找轮廓(仅检测外部轮廓,保留所有轮廓点)
    contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 复制原图用于绘制轮廓(避免修改原图)
    img_contours = img.copy()

    # 设定最小轮廓面积(像素),过滤噪声
    min_area = 500

    # 遍历所有轮廓
    for i, cnt in enumerate(contours):
        area = cv2.contourArea(cnt)
        if area < min_area:
            continue

        # 计算外接矩形
        x, y, w, h = cv2.boundingRect(cnt)

        # 计算轮廓的矩,用于求中心点
        M = cv2.moments(cnt)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
        else:
            cx, cy = x + w//2, y + h//2

        # 绘制轮廓线(绿色)
        cv2.drawContours(img_contours, [cnt], -1, (0, 255, 0), 2)

        # 绘制外接矩形(蓝色)
        cv2.rectangle(img_contours, (x, y), (x+w, y+h), (255, 0, 0), 2)

        # 绘制中心点(红色)
        cv2.circle(img_contours, (cx, cy), 5, (0, 0, 255), -1)

        # 显示轮廓编号和面积
        cv2.putText(img_contours, f"#{i+1} area:{int(area)}", (x, y-10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

在本实验中这个并不是重点,所以我们只需要知道如何使用相应的函数,每个函数有什么功能,以及如何填写参数即可,不必深究原理。

六、项目总结

        先展示最后的效果,我选定了红黄绿,三种颜色,用来模拟红绿灯,为下一个实验做一些铺垫,这些代码都是经过多次调试以后的,但是我无法保证是最优的,所以,一定要多多调试。

七、优化方向

通过实践操作加深对代码逻辑的理解,能够帮助筛选出更优的解决方案。当前项目仍处于基础阶段,主要基于理想条件进行设计,尚未覆盖复杂环境下的实际应用需求。

环境适应性优化方向

有以下的环境是我思考的可能会出现的问题和未来完善的方向
1、需测试夜间或光线不足场景下的识别效果,考虑引入低光增强算法或红外辅助技术

2、针对摄像头晃动或物体移动的情况,优化图像稳定性和动态目标追踪能力

3、当前可能受限于特定颜色范围,需扩展色彩识别阈值,验证相近色系的区分度,避免误操作

4、一些更加恶劣的天气,比如华南地区的台风,北京的雾霾,在这些环境是否能正常识别,等等都是需要进行迭代来实现具体的算法

参数调优

还有就是在调参方面,我们未来可以思考的方向,比如

1、引入机器学习和深度学习,实现自动调参,减少人工干预

2、噪声干扰,这是实际操作中出现的问题,我截下来的结果仅是当时噪声最小的情况的图片,可以使用像早停、贝叶斯优化、多次实验等方法进行优化

3、 针对红色的HSV环绕特性,采用双阈值范围合并策略,提升红色检测的完整性

八、心得与体会

无论是什么方向进行优化抑或是添加何种功能,都需要自己动手操作,动手优化,动脑思考才能有所收获。

九、完整代码

调试代码(HSV参数选择)

import cv2
import numpy as np

# 摄像头设置
frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(1)
cap.set(3, frameWidth)
cap.set(4, frameHeight)
cap.set(10, 150)   # 亮度

# ---------- 创建控制面板窗口和滑动条 ----------
cv2.namedWindow("TrackBar")   # 必须先创建窗口

# 创建6个滑动条,参数:名称, 窗口名, 初始值, 最大值, 回调函数
cv2.createTrackbar("Hue Min", "TrackBar", 0, 179, lambda x: None)
cv2.createTrackbar("Hue Max", "TrackBar", 179, 179, lambda x: None)
cv2.createTrackbar("Saturation Min", "TrackBar", 0, 255, lambda x: None)
cv2.createTrackbar("Saturation Max", "TrackBar", 255, 255, lambda x: None)
cv2.createTrackbar("Value Min", "TrackBar", 0, 255, lambda x: None)
cv2.createTrackbar("Value Max", "TrackBar", 255, 255, lambda x: None)

# 可选:为显示窗口设置较小尺寸(解决窗口太大的问题)
cv2.namedWindow("Original", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Original", 320, 240)
cv2.namedWindow("Mask", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Mask", 320, 240)
cv2.namedWindow("Result", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Result", 320, 240)

while True:
    # ===================== 修复代码:检测窗口是否关闭 =====================
    # 如果TrackBar窗口被关闭,直接退出循环
    if cv2.getWindowProperty("TrackBar", cv2.WND_PROP_VISIBLE) < 1:
        break
    # ====================================================================

    success, img = cap.read()
    if not success:
        print("无法获取摄像头画面")
        break

    # 转换到HSV空间
    imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 获取当前滑动条的值
    h_min = cv2.getTrackbarPos("Hue Min", "TrackBar")
    h_max = cv2.getTrackbarPos("Hue Max", "TrackBar")
    s_min = cv2.getTrackbarPos("Saturation Min", "TrackBar")
    s_max = cv2.getTrackbarPos("Saturation Max", "TrackBar")
    v_min = cv2.getTrackbarPos("Value Min", "TrackBar")
    v_max = cv2.getTrackbarPos("Value Max", "TrackBar")

    lower = np.array([h_min, s_min, v_min])
    upper = np.array([h_max, s_max, v_max])

    # 生成掩膜并应用
    mask = cv2.inRange(imgHSV, lower, upper)
    imgResult = cv2.bitwise_and(img, img, mask=mask)

    # 显示三个窗口
    cv2.imshow("Original", img)
    cv2.imshow("Mask", mask)
    cv2.imshow("Result", imgResult)

    # 按 'q' 退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

实现代码(完整功能)

import cv2
import numpy as np

# 摄像头设置
frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(1)  # 如果摄像头索引不对,请改成 0
cap.set(3, frameWidth)
cap.set(4, frameHeight)
cap.set(10, 150)   # 亮度

# 预设颜色阈值 (格式: [H_min, H_max, S_min, S_max, V_min, V_max])
#第一次,噪声点太多,舍弃
#myColors = [
  # [174, 179, 131, 243, 117, 241],   # 红色
 #   [15, 91, 8, 244, 51, 163],        # 绿色
#   [12, 28, 109, 221, 118, 214]      # 黄色
#]

#第二次,差太多
#myColors = [
 #   [168, 179, 134, 255, 152, 254],   # 红色
#    [15, 91, 8, 244, 51, 163],        # 绿色
#    [12, 28, 109, 221, 118, 214]      # 黄色
#]
myColors = [
    #[171, 179, 94, 218, 139, 255],
    #[168, 179, 134, 255, 152, 254],# 红色
#[174, 179, 131, 243, 117, 241],
    [137,179,118,237,112,232],
    #[173,179,70,25,5,149,255],
    #[58, 87, 43, 178, 167, 243],
#    [56,73,75,124,89,255],# 绿色
    [40,85,53,133,135,255],
   # [19, 31, 216, 255, 160, 255]      # 黄色
[12, 28, 109, 221, 118, 214]
]
# 创建显示窗口并调整大小
cv2.namedWindow("Original", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Original", 320, 240)
cv2.namedWindow("Mask", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Mask", 320, 240)
cv2.namedWindow("Result", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Result", 320, 240)
cv2.namedWindow("Contours", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Contours", 320, 240)

def get_combined_mask(img_hsv):
    """根据预设颜色列表生成合并后的掩膜"""
    combined_mask = np.zeros((img_hsv.shape[0], img_hsv.shape[1]), dtype=np.uint8)
    for color in myColors:
        lower = np.array([color[0], color[2], color[4]])   # H_min, S_min, V_min
        upper = np.array([color[1], color[3], color[5]])   # H_max, S_max, V_max
        mask = cv2.inRange(img_hsv, lower, upper)
        combined_mask = cv2.bitwise_or(combined_mask, mask)
    return combined_mask

while True:
    success, img = cap.read()
    if not success:
        print("无法获取摄像头画面")
        break

    # 转换为 HSV 空间
    imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 生成合并掩膜
    mask = get_combined_mask(imgHSV)

    # 可选:形态学操作去除噪点,使掩膜更干净
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)  # 先腐蚀后膨胀,去除小噪点
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 先膨胀后腐蚀,填补小空洞

    # 将掩膜应用到原图,得到只显示检测颜色的结果
    result = cv2.bitwise_and(img, img, mask=mask)

    # ---------- 轮廓检测 ----------
    # 查找轮廓(仅检测外部轮廓,保留所有轮廓点)
    contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 复制原图用于绘制轮廓(避免修改原图)
    img_contours = img.copy()

    # 设定最小轮廓面积(像素),过滤噪声
    min_area = 500

    # 遍历所有轮廓
    for i, cnt in enumerate(contours):
        area = cv2.contourArea(cnt)
        if area < min_area:
            continue

        # 计算外接矩形
        x, y, w, h = cv2.boundingRect(cnt)

        # 计算轮廓的矩,用于求中心点
        M = cv2.moments(cnt)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
        else:
            cx, cy = x + w//2, y + h//2

        # 绘制轮廓线(绿色)
        cv2.drawContours(img_contours, [cnt], -1, (0, 255, 0), 2)

        # 绘制外接矩形(蓝色)
        cv2.rectangle(img_contours, (x, y), (x+w, y+h), (255, 0, 0), 2)

        # 绘制中心点(红色)
        cv2.circle(img_contours, (cx, cy), 5, (0, 0, 255), -1)

        # 显示轮廓编号和面积
        cv2.putText(img_contours, f"#{i+1} area:{int(area)}", (x, y-10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

    # ---------- 显示图像 ----------
    cv2.imshow("Original", img)
    cv2.imshow("Mask", mask)
    cv2.imshow("Result", result)
    cv2.imshow("Contours", img_contours)

    # 按 'q' 退出
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐