Python OpenCV: RGB三色识别的最佳工程实践

Python OpenCV: RGB三色识别的最佳工程实践

在备赛工程实践创新能力大赛(工创)智能物流赛道的过程中,需要一套识别红绿蓝三色的识别代码,在我第一年参赛的时候,我就已经注意到我需要频繁的调整参数,包括我的学长也都是这样,这个流程已经习以为常了,但是上场调参往往是紧张而痛苦的,因为调试时间很短,所以也常常出现一些调参失误。

于是我开始思考有没有一种方案可以不再需要重复性的调参,不需要换一个场地或者光线强度就需要调参。

经过一些观察,我注意到HSV颜色识别中,只要肉眼可以分辨这个颜色,就一定有一个阈值参数将这个颜色区分开,那按照理论来说也一定有一套参数可以确保在摄像头画面中,只要人可以区分就一定可以用的参数。

而这个问题我如今已经解决,我在第二年参赛(2025)就使用了这套方案,在现场的表现也非常不错,我没有花费任何时间在颜色识别的参数上,大大提高了识别的准确性和效率。

传统 HSV 颜色识别的痛点

在 OpenCV 中,我们通常使用 HSV (色相、饱和度、明度) 颜色空间进行颜色识别,因为它比 RGB 更接近人类对颜色的感知。然而,在实际应用中,我发现了一个关键问题:

红色识别的困境

红色在 HSV 颜色空间中位于色相 (H) 通道的两端。H 通道的取值范围是 0-180(OpenCV 中),而红色对应的色相值大约在 0° 附近。这意味着红色实际上跨越了 0° 边界,形成了两个不连续的区域:

  • 区域 1: 0° - 15° 左右
  • 区域 2: 165° - 180° 左右

传统的解决方案是为红色设置两个不同的阈值范围,然后对两个范围的结果进行或运算。这种方法不仅代码冗余,而且难以维护 - 每次调整红色阈值时,都需要修改两个范围的值。

色环概念的引入

要理解这个问题,我们需要先了解色相 (H) 在 HSV 颜色空间中的本质:色相实际上是一个 180 度的环形

想象一下,将 0° 和 180° 连接起来,形成一个圆环。这样,红色就不再是两个不连续的区域,而是一个连续的区域,只是这个区域跨越了圆环的起点和终点。

解决方案:中心点和容差的概念

基于色环的理解,我设计了一个 TraditionalColorDetector 类,通过引入中心点容差的概念来简化颜色识别的参数管理。

核心思路

  1. 中心点 (centre):表示目标颜色在色相环上的中心位置
  2. 容差 (error):表示从中心点向两侧扩展的范围

这样,无论目标颜色是否跨越 0° 边界,我们只需要设置一个中心点和一个容差,系统会自动计算出正确的阈值范围。

关键代码实现

python 复制代码
class TraditionalColorDetector:
    """
    传统颜色识别
    ----
    使用中央色相阈值和色相容差来识别颜色
    """

    LOW_H1: int
    UP_H1: int

    LOW_H2: int | None
    UP_H2: int | None

    centre: int = 65
    error: int = 10

    # 颜色阈值配置
    color_threshold = {
        "R": {
            "centre": 0,
            "error": 12,
            "L_S": 20,
            "U_S": 255,
            "L_V": 0,
            "U_V": 255
        },
        "G": {
            "centre": 69,
            "error": 12,
            "L_S": 20,
            "U_S": 255,
            "L_V": 30,
            "U_V": 255
        },
        "B": {
            "centre": 108,
            "error": 11,
            "L_S": 100,
            "U_S": 255,
            "L_V": 0,
            "U_V": 255
        }
    }

    def update_range(self, color_name: str = "R"):
        """
        根据中心点和容差更新色相范围
        """
        self.centre = self.color_threshold[color_name]["centre"]
        self.error = self.color_threshold[color_name]["error"]

        minH = self.centre - self.error
        maxH = self.centre + self.error

        if minH < 0:
            # 跨越 0° 边界的情况(如红色)
            self.LOW_H2 = 180 + minH
            self.UP_H2 = 180

            self.LOW_H1 = 0
            self.UP_H1 = maxH
        elif maxH > 180:
            # 跨越 180° 边界的情况
            self.LOW_H2 = 0
            self.UP_H2 = maxH - 180

            self.LOW_H1 = minH
            self.UP_H1 = 180
        else:
            # 不跨越边界的情况
            self.LOW_H1 = minH
            self.UP_H1 = maxH

            self.LOW_H2 = None
            self.UP_H2 = None

二值化处理

有了正确的阈值范围后,我们可以进行二值化处理:

python 复制代码
def binarization(self, _img: cv2.typing.MatLike) -> cv2.typing.MatLike:
    """
    二值化
    ----
    Args:
        _img(cv2.typing.MatLike): 输入图像
    Returns:
        cv2.typing.MatLike: 二值化后的图像
    """
    img = _img.copy()
    # 高斯滤波
    img = cv2.GaussianBlur(img, (15, 15), 2)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    if self.LOW_H2 is None:
        # 单一范围
        low = np.array([self.LOW_H1, self.L_S, self.L_V])
        up = np.array([self.UP_H1, self.U_S, self.U_V])
        mask = cv2.inRange(hsv, low, up)
    else:
        # 两个范围(如红色)
        low1 = np.array([self.LOW_H1, self.L_S, self.L_V])
        up1 = np.array([self.UP_H1, self.U_S, self.U_V])

        low2 = np.array([self.LOW_H2, self.L_S, self.L_V])
        up2 = np.array([self.UP_H2, self.U_S, self.U_V])

        mask1 = cv2.inRange(hsv, low1, up1)
        mask2 = cv2.inRange(hsv, low2, up2)
        mask = cv2.bitwise_or(mask1, mask2)

    # 形态学闭操作
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1)

    return mask

获取颜色位置

最后,我们可以通过二值化图像获取颜色的位置:

python 复制代码
def get_color_position(self, binarized_img:cv2.typing.MatLike) -> tuple[int, int, int, int] | None:
    """
    获取颜色位置
    ----
    通过传入二值化的图像,然后取外接矩形的中心点作为颜色的位置

    Args:
        binarized_img(cv2.typing.MatLike): 二值化图像
    Returns:
        res(tuple[int, int, int, int]): 颜色中心点位置x,y和外接矩形的宽和高
    """
    contours, _ = cv2.findContours(binarized_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours:
        # 获取符合面积要求的轮廓
        valid_contours = [cnt for cnt in contours if self.min_material_area <= cv2.contourArea(cnt) <= self.max_material_area]
        if valid_contours:
            # 获取最大的符合面积要求的轮廓
            largest_contour = max(valid_contours, key=cv2.contourArea)
            # 获取外接矩形
            x, y, w, h = cv2.boundingRect(largest_contour)
            # 计算矩形中心点
            center_x = x + w // 2
            center_y = y + h // 2

            return center_x, center_y, w, h
    return None

使用示例

下面是一个完整的使用示例:

python 复制代码
import cv2
import numpy as np
from ColorDetect import TraditionalColorDetector

# 初始化颜色检测器
detector = TraditionalColorDetector()

# 读取图像
img = cv2.imread('test_image.jpg')

# 检测红色
detector.update_threshold("R")
red_mask = detector.binarization(img)
red_position = detector.get_color_position(red_mask)

# 检测绿色
detector.update_threshold("G")
green_mask = detector.binarization(img)
green_position = detector.get_color_position(green_mask)

# 检测蓝色
detector.update_threshold("B")
blue_mask = detector.binarization(img)
blue_position = detector.get_color_position(blue_mask)

# 可视化结果
if red_position:
    cx, cy, w, h = red_position
    cv2.rectangle(img, (cx - w//2, cy - h//2), (cx + w//2, cy + h//2), (0, 0, 255), 2)
    cv2.putText(img, "Red", (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

if green_position:
    cx, cy, w, h = green_position
    cv2.rectangle(img, (cx - w//2, cy - h//2), (cx + w//2, cy + h//2), (0, 255, 0), 2)
    cv2.putText(img, "Green", (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

if blue_position:
    cx, cy, w, h = blue_position
    cv2.rectangle(img, (cx - w//2, cy - h//2), (cx + w//2, cy + h//2), (255, 0, 0), 2)
    cv2.putText(img, "Blue", (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)

# 显示结果
cv2.imshow("Result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

优势与特点

  1. 简化参数管理:只需设置中心点和容差,无需关心是否跨越 0° 边界
  2. 代码复用性高:统一的处理逻辑适用于所有颜色
  3. 可扩展性强:易于添加新的颜色配置

调参技巧

  1. 中心点选择

    • 红色:0° 左右
    • 绿色:60°-70° 左右
    • 蓝色:100°-120° 左右
  2. 容差调整

    • 容差越大,识别范围越广,但可能引入误识别
    • 容差越小,识别精度越高,但可能漏识别
    • 建议从 10-15 开始调整
  3. 饱和度和明度,这个主要用于排除黑色和白色:

    • 饱和度 (S):控制颜色的鲜艳程度,通常设置一个较低的下限。如果白色被错误的识别了,就调高这个值下限
    • 明度 (V):控制颜色的明暗程度,根据实际场景调整,如果黑色被错误识别了,就提高这个值的下限
相关推荐
haosend4 小时前
AI时代,传统网络运维人员的转型指南
python·数据网络·网络自动化
曲幽4 小时前
不止于JWT:用FastAPI的Depends实现细粒度权限控制
python·fastapi·web·jwt·rbac·permission·depends·abac
IVEN_1 天前
只会Python皮毛?深入理解这几点,轻松进阶全栈开发
python·全栈
Ray Liang1 天前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
AI攻城狮1 天前
如何给 AI Agent 做"断舍离":OpenClaw Session 自动清理实践
python
千寻girling1 天前
一份不可多得的 《 Python 》语言教程
人工智能·后端·python
AI攻城狮1 天前
用 Playwright 实现博客一键发布到稀土掘金
python·自动化运维
曲幽1 天前
FastAPI分布式系统实战:拆解分布式系统中常见问题及解决方案
redis·python·fastapi·web·httpx·lock·asyncio