在计算机视觉项目中,颜色分割是一个基础但至关重要的环节。无论是魔方机器人、交通信号灯识别还是皮肤检测系统,准确的颜色识别都直接影响最终效果。然而很多开发者(包括曾经的我)都踩过同样的坑——在实验室调试完美的颜色阈值,一到实际环境就完全失效。这通常是因为我们忽略了不同光照条件下颜色表现的巨大差异。
最近帮朋友Mark调试他的魔方机器人时,我们系统性地测试了RGB、LAB、YCrCb和HSV四种色彩空间在不同光照下的表现。本文将分享这个过程中的关键发现和实用技巧,包含完整的C++和Python代码实现。无论你正在开发哪种颜色相关的计算机视觉应用,这些经验都能帮你少走弯路。
我们使用了两组魔方图像进行对比测试:
python复制# Python读取图像示例
bright = cv2.imread('cube_sunlight.jpg') # 室外图像
dark = cv2.imread('cube_indoor.jpg') # 室内图像
cpp复制// C++读取图像示例
cv::Mat bright = cv::imread("cube_sunlight.jpg");
cv::Mat dark = cv::imread("cube_indoor.jpg");
重要提示:OpenCV默认以BGR格式读取图像,而非常见的RGB顺序。这在进行颜色转换时要特别注意。
RGB是最直观的色彩空间,但存在两个致命缺陷:
通过分离通道可以直观看到问题:
python复制b, g, r = cv2.split(bright)
cv2.imshow("Blue Channel", b)
cv2.imshow("Green Channel", g)
cv2.imshow("Red Channel", r)

图示:同一魔方在室外(上)和室内(下)的RGB通道对比,注意蓝色通道的显著差异
LAB色彩空间由三个分量组成:
转换方法:
python复制bright_lab = cv2.cvtColor(bright, cv2.COLOR_BGR2LAB)
dark_lab = cv2.cvtColor(dark, cv2.COLOR_BGR2LAB)
cpp复制cv::Mat bright_lab, dark_lab;
cv::cvtColor(bright, bright_lab, cv::COLOR_BGR2LAB);
cv2::cvtColor(dark, dark_lab, cv::COLOR_BGR2LAB);
关键优势:
YCrCb常用于视频压缩,包含:
转换代码:
python复制bright_ycrcb = cv2.cvtColor(bright, cv2.COLOR_BGR2YCrCb)
cpp复制cv::Mat bright_ycrcb;
cv::cvtColor(bright, bright_ycrcb, cv::COLOR_BGR2YCrCb);
HSV色彩空间最符合人类对颜色的直观描述:
转换实现:
python复制bright_hsv = cv2.cvtColor(bright, cv2.COLOR_BGR2HSV)
最简单的颜色分割流程:
python复制green_bgr = [40, 158, 16] # 绿色BGR值
threshold = 40
min_bgr = np.array([green_bgr[0]-threshold, green_bgr[1]-threshold, green_bgr[2]-threshold])
max_bgr = np.array([green_bgr[0]+threshold, green_bgr[1]+threshold, green_bgr[2]+threshold])
mask = cv2.inRange(bright, min_bgr, max_bgr)
result = cv2.bitwise_and(bright, bright, mask=mask)
常见陷阱:直接使用RGB空间且固定阈值,在不同光照下必然失败。下图展示了室内外环境使用相同阈值的分割结果差异:

更可靠的方法是采集多光照条件下的样本,分析颜色分布:
python复制# 收集多个蓝色样本图像的像素值
blue_pixels = []
for img_path in blue_samples:
img = cv2.imread(img_path)
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
a = lab[:,:,1].flatten()
b = lab[:,:,2].flatten()
blue_pixels.extend(zip(a,b))
# 绘制二维直方图
plt.hist2d([p[0] for p in blue_pixels], [p[1] for p in blue_pixels], bins=50)
plt.xlabel('A channel')
plt.ylabel('B channel')
通过分析分布图,可以确定更鲁棒的阈值范围:

实际项目中,可以组合多个色彩空间的优势:
python复制# 组合HSV和LAB的分割
hsv_mask = cv2.inRange(hsv_img, hsv_min, hsv_max)
lab_mask = cv2.inRange(lab_img, lab_min, lab_max)
combined_mask = cv2.bitwise_and(hsv_mask, lab_mask)
# 形态学优化
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
refined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel)
python复制def robust_color_segmentation(image, target_color):
# 转换为各色彩空间
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# 根据目标颜色设置阈值
if target_color == 'green':
# HSV阈值(H范围需要特别处理)
hsv_low = (35, 50, 50)
hsv_high = (85, 255, 255)
# LAB阈值
lab_low = (0, -128, 50)
lab_high = (255, -30, 127)
elif target_color == 'red':
# 红色在HSV中位于0°和180°附近
hsv_low1 = (0, 50, 50)
hsv_high1 = (10, 255, 255)
hsv_low2 = (170, 50, 50)
hsv_high2 = (180, 255, 255)
# LAB阈值
lab_low = (0, 100, 20)
lab_high = (255, 127, 127)
# 应用阈值
if target_color == 'red':
mask1 = cv2.inRange(hsv, hsv_low1, hsv_high1)
mask2 = cv2.inRange(hsv, hsv_low2, hsv_high2)
hsv_mask = cv2.bitwise_or(mask1, mask2)
else:
hsv_mask = cv2.inRange(hsv, hsv_low, hsv_high)
lab_mask = cv2.inRange(lab, lab_low, lab_high)
# 组合掩模
combined = cv2.bitwise_and(hsv_mask, lab_mask)
# 后处理
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
refined = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel)
return refined
cpp复制cv::Mat robustColorSegmentation(cv::Mat image, std::string targetColor) {
cv::Mat lab, hsv;
cv::cvtColor(image, lab, cv::COLOR_BGR2LAB);
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
cv::Mat mask1, mask2, hsvMask, labMask, combined;
if (targetColor == "green") {
cv::inRange(hsv, cv::Scalar(35, 50, 50), cv::Scalar(85, 255, 255), hsvMask);
cv::inRange(lab, cv::Scalar(0, -128, 50), cv::Scalar(255, -30, 127), labMask);
}
else if (targetColor == "red") {
cv::inRange(hsv, cv::Scalar(0, 50, 50), cv::Scalar(10, 255, 255), mask1);
cv::inRange(hsv, cv::Scalar(170, 50, 50), cv::Scalar(180, 255, 255), mask2);
cv::bitwise_or(mask1, mask2, hsvMask);
cv::inRange(lab, cv::Scalar(0, 100, 20), cv::Scalar(255, 127, 127), labMask);
}
cv::bitwise_and(hsvMask, labMask, combined);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5));
cv::morphologyEx(combined, combined, cv::MORPH_CLOSE, kernel);
return combined;
}
在颜色分割前,可以进行光照归一化处理:
python复制def normalize_illumination(image):
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# CLAHE对比度受限的自适应直方图均衡化
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
l_norm = clahe.apply(l)
lab_norm = cv2.merge((l_norm, a, b))
bgr_norm = cv2.cvtColor(lab_norm, cv2.COLOR_LAB2BGR)
return bgr_norm
对于变化的光照环境,可以实现动态阈值:
python复制def dynamic_threshold(image, target_hue, sat_thresh=40):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
# 基于图像平均亮度调整V通道阈值
mean_v = np.mean(v)
if mean_v < 50: # 低光照
v_low = 30
v_high = 150
elif mean_v > 200: # 强光照
v_low = 100
v_high = 255
else: # 中等光照
v_low = 50
v_high = 230
# 处理红色等需要跨0°的颜色
if target_hue == 'red':
mask1 = cv2.inRange(hsv, (0, sat_thresh, v_low), (10, 255, v_high))
mask2 = cv2.inRange(hsv, (170, sat_thresh, v_low), (180, 255, v_high))
mask = cv2.bitwise_or(mask1, mask2)
else:
hue_range = 15
hue_val = {'green':60, 'blue':120, 'yellow':30}[target_hue]
mask = cv2.inRange(hsv, (hue_val-hue_range, sat_thresh, v_low),
(hue_val+hue_range, 255, v_high))
return mask
查表法(LUT):对于固定的颜色转换,可以预先计算查找表
python复制def create_hsv_lut():
lut = np.zeros((256, 1), dtype=np.uint8)
for i in range(256):
if i < 10 or i > 170: # 红色范围
lut[i][0] = 255
return lut
hsv_lut = create_hsv_lut()
h_channel = cv2.LUT(hsv[:,:,0], hsv_lut)
ROI处理:只在感兴趣区域进行颜色分割
分辨率降低:对大图像先缩小处理再放大结果
python复制ycrcb_low = (0, 135, 85)
ycrcb_high = (255, 180, 135)
可能原因:
解决方案:
python复制kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
可能原因:
解决方案:
可能原因:
解决方案:
模拟人类视觉系统在不同光照下识别相同颜色的能力:
python复制def gray_world_normalization(image):
avg_b = np.mean(image[:,:,0])
avg_g = np.mean(image[:,:,1])
avg_r = np.mean(image[:,:,2])
avg_gray = (avg_b + avg_g + avg_r) / 3
scale_b = avg_gray / avg_b
scale_g = avg_gray / avg_g
scale_r = avg_gray / avg_r
normalized = image.copy()
normalized[:,:,0] = np.clip(image[:,:,0] * scale_b, 0, 255)
normalized[:,:,1] = np.clip(image[:,:,1] * scale_g, 0, 255)
normalized[:,:,2] = np.clip(image[:,:,2] * scale_r, 0, 255)
return normalized.astype(np.uint8)
当传统方法不能满足需求时,可以考虑:
K-means聚类:自动发现图像中的主要颜色
python复制def kmeans_color_segmentation(image, k=4):
pixels = image.reshape((-1,3)).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.2)
_, labels, centers = cv2.kmeans(pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
centers = np.uint8(centers)
segmented = centers[labels.flatten()].reshape(image.shape)
return segmented
深度学习模型:训练一个端到端的颜色分割网络
对于复杂光照环境,可以使用:
在实际的魔方机器人项目中,我们最终采用了LAB色彩空间结合动态阈值的方案,配合适当的光照预处理,在不同环境下都能达到95%以上的颜色识别准确率。关键是要根据具体应用场景,选择合适的色彩空间和参数调整策略。