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 类,通过引入中心点 和容差的概念来简化颜色识别的参数管理。
核心思路
- 中心点 (centre):表示目标颜色在色相环上的中心位置
- 容差 (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()
优势与特点
- 简化参数管理:只需设置中心点和容差,无需关心是否跨越 0° 边界
- 代码复用性高:统一的处理逻辑适用于所有颜色
- 可扩展性强:易于添加新的颜色配置
调参技巧
-
中心点选择:
- 红色:0° 左右
- 绿色:60°-70° 左右
- 蓝色:100°-120° 左右
-
容差调整:
- 容差越大,识别范围越广,但可能引入误识别
- 容差越小,识别精度越高,但可能漏识别
- 建议从 10-15 开始调整
-
饱和度和明度,这个主要用于排除黑色和白色:
- 饱和度 (S):控制颜色的鲜艳程度,通常设置一个较低的下限。如果白色被错误的识别了,就调高这个值下限
- 明度 (V):控制颜色的明暗程度,根据实际场景调整,如果黑色被错误识别了,就提高这个值的下限