图像处理基础 第5课/共35课

第05课:色彩空间

📖 课程概述

本课学习色彩空间的核心原理与实现。我们将从数学基础出发,通过代码实践真正理解每一个算法的运作机制。

📑 本课目录

1. RGB与BGR2. HSV色彩空间3. LAB色彩空间4. YCrCb色彩空间5. 色彩空间转换原理6. 颜色分割实战7. 白平衡与色彩校正

1. RGB与BGR

RGB是最直观的色彩模型。OpenCV默认使用BGR顺序,这是最常见的坑。

RGB加色模型: C = R·er + G·eg + B·eb
灰度转换: Y = 0.299R + 0.587G + 0.114B
import cv2, numpy as np
img_bgr = np.zeros((200,400,3), dtype=np.uint8)
img_bgr[50:150, 50:150] = [0, 0, 255]  # BGR红色
img_bgr[50:150, 200:300] = [255, 0, 0]  # BGR蓝色
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
print(f"BGR红色区域: {img_bgr[100,100]}")  # [0, 0, 255]
print(f"RGB红色区域: {img_rgb[100,100]}")  # [255, 0, 0]
a = np.array([200], dtype=np.uint8)
print(f"NumPy加法 200+100 = {(a+100).item()}")  # 溢出: 44
print(f"cv2.add 200+100 = {cv2.add(a, np.array([100],dtype=np.uint8)).item()}")  # 饱和: 255
cv2.imwrite('/var/www/ttl/cv/l05_bgr.png', np.hstack([img_bgr, img_rgb]))

2. HSV色彩空间

HSV将颜色分解为色调(H)、饱和度(S)、明度(V),更符合人类感知。

H ∈ [0°, 360°] → OpenCV中 [0, 180]
S ∈ [0%, 100%] → OpenCV中 [0, 255]
V ∈ [0%, 100%] → OpenCV中 [0, 255]
import cv2, numpy as np
hsv_img = np.zeros((180,360,3), dtype=np.uint8)
for h in range(180):
    for s in range(0,360,2):
        hsv_img[h, s:s+2] = [h, 255, 255]
bgr = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)
cv2.imwrite('/var/www/ttl/cv/l05_hsv.png', bgr)

# 红色分割(需两个范围)
test = np.zeros((200,200,3), dtype=np.uint8)
test[:] = [0, 0, 255]
hsv = cv2.cvtColor(test, cv2.COLOR_BGR2HSV)
mask1 = cv2.inRange(hsv, np.array([0,100,100]), np.array([10,255,255]))
mask2 = cv2.inRange(hsv, np.array([170,100,100]), np.array([180,255,255]))
red_mask = mask1 | mask2
print(f"红色分割像素: {np.count_nonzero(red_mask)}")

3. LAB色彩空间

LAB色彩空间设计目标是感知均匀

L* ∈ [0, 100]: 亮度
a* ∈ [-128, 127]: 红绿轴
b* ∈ [-128, 127]: 蓝黄轴
色差: ΔE = √(ΔL*² + Δa*² + Δb*²)
import cv2, numpy as np
c1 = np.uint8([[[50,100,200]]])  # BGR
c2 = np.uint8([[[55,105,210]]])  # BGR相似色
lab1 = cv2.cvtColor(c1, cv2.COLOR_BGR2LAB).astype(float)
lab2 = cv2.cvtColor(c2, cv2.COLOR_BGR2LAB).astype(float)
de = np.sqrt(np.sum((lab1-lab2)**2))
print(f"相似色色差 DeltaE = {de:.2f}")
c3 = np.uint8([[[0,255,0]]])  # 绿色
lab3 = cv2.cvtColor(c3, cv2.COLOR_BGR2LAB).astype(float)
de2 = np.sqrt(np.sum((lab1-lab3)**2))
print(f"不同色色差 DeltaE = {de2:.2f}")

4. YCrCb色彩空间

Y = 0.299R + 0.587G + 0.114B
肤色范围: Cr ∈ [133, 173], Cb ∈ [77, 127]
import cv2, numpy as np
skin_bgr = np.uint8([[[80, 140, 200]]])  # 近似肤色
ycrcb = cv2.cvtColor(skin_bgr, cv2.COLOR_BGR2YCrCb)
y, cr, cb = ycrcb[0,0]
is_skin = (133 <= cr <= 173) and (77 <= cb <= 127)
print(f"肤色BGR[80,140,200] -> YCrCb: Y={y},Cr={cr},Cb={cb} -> {'肤色' if is_skin else '非肤色'}")

5. 色彩空间转换原理

import cv2, numpy as np

def bgr_to_hsv_manual(bgr):
    b, g, r = bgr.astype(float) / 255.0
    cmax, cmin = max(r,g,b), min(r,g,b)
    delta = cmax - cmin
    h = 0 if delta == 0 else (60*((g-b)/delta)%6 if cmax==r else 60*((b-r)/delta+2) if cmax==g else 60*((r-g)/delta+4))
    s = 0 if cmax == 0 else delta/cmax
    v = cmax
    return np.array([h/2, s*255, v*255])

for bgr_val in [[0,0,255], [0,255,0], [255,0,0]]:
    pixel = np.uint8([[bgr_val]])
    opencv_hsv = cv2.cvtColor(pixel, cv2.COLOR_BGR2HSV)[0][0]
    manual_hsv = bgr_to_hsv_manual(np.array(bgr_val, dtype=np.uint8))
    print(f"BGR{bgr_val} -> OpenCV:{opencv_hsv}, 手动:{manual_hsv}")

6. 颜色分割实战

import cv2, numpy as np
img = np.zeros((300,500,3), dtype=np.uint8)
cv2.circle(img, (120,150), 60, (0,0,255), -1)  # 红色禁止标志
pts = np.array([[350,80],[420,200],[280,200]])
cv2.fillPoly(img, [pts], (0,215,255))  # 黄色警告
cv2.rectangle(img, (250,220), (400,280), (255,100,0), -1)  # 蓝色指示
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask_r1 = cv2.inRange(hsv, np.array([0,100,100]), np.array([10,255,255]))
mask_r2 = cv2.inRange(hsv, np.array([170,100,100]), np.array([180,255,255]))
red = mask_r1 | mask_r2
yellow = cv2.inRange(hsv, np.array([20,100,100]), np.array([35,255,255]))
blue = cv2.inRange(hsv, np.array([100,100,100]), np.array([130,255,255]))
for name, mask in [('红色',red),('黄色',yellow),('蓝色',blue)]:
    cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    print(f"{name}: {len(cnts)}个区域")
cv2.imwrite('/var/www/ttl/cv/l05_traffic.png', img)

7. 白平衡与色彩校正

灰度世界假设:图像所有像素的平均值应该为灰色(R=G=B)。
import cv2, numpy as np
img = np.zeros((256,256,3), dtype=np.uint8)
for i in range(256):
    img[i,:] = [int(i*0.8), int(i*0.9), int(min(i*1.3,255))]  # 偏蓝
print(f"偏色: B={img[:,:,0].mean():.1f}, G={img[:,:,1].mean():.1f}, R={img[:,:,2].mean():.1f}")

def gray_world(img):
    result = img.astype(np.float64)
    avg = [result[:,:,c].mean() for c in range(3)]
    gray_avg = sum(avg) / 3
    for c in range(3):
        result[:,:,c] = np.clip(result[:,:,c] * gray_avg / avg[c], 0, 255)
    return result.astype(np.uint8)

balanced = gray_world(img)
print(f"校正: B={balanced[:,:,0].mean():.1f}, G={balanced[:,:,1].mean():.1f}, R={balanced[:,:,2].mean():.1f}")
cv2.imwrite('/var/www/ttl/cv/l05_wb.png', np.hstack([img, balanced]))

🔑 色彩空间核心要点

📝 课后练习

  1. 实现本课所有算法并对比效果
  2. 在不同参数下测试算法的鲁棒性
  3. 将本课方法应用到自己的数据集
  4. 分析算法的计算复杂度和优化方向
延伸阅读:推荐阅读《Digital Image Processing》(Gonzalez)相关章节,以及OpenCV官方文档中的详细API说明。实际项目中,建议先在简单数据上验证算法,再迁移到复杂场景。

📊 方法对比总结

方法优点缺点适用场景
方法A简单高效精度有限快速原型
方法B精度高计算量大离线处理
方法C平衡精度与速度参数调优复杂实际应用

📐 关键公式速查

本课涉及的核心数学公式汇总,方便快速参考:

🔬 深入理解:色彩空间转换的数学

RGB到HSV的转换不是线性的,需要注意实现细节:

RGB→HSV详细公式:
V = max(R,G,B)
S = 0 if V=0, else (V-min(R,G,B))/V
H = 60° × { (G-B)/(V-min) if V=R; 2+(B-R)/(V-min) if V=G; 4+(R-G)/(V-min) if V=B }
H = H + 360° if H < 0
import cv2, numpy as np

# 完整的BGR→HSV转换验证
test_colors = {
    '红': [0,0,255], '绿': [0,255,0], '蓝': [255,0,0],
    '黄': [0,255,255], '紫': [255,0,255], '青': [255,255,0],
    '白': [255,255,255], '黑': [0,0,0], '灰': [128,128,128]
}

print("BGR → HSV 转换表:")
print(f"{'颜色':>4} | {'B':>3} {'G':>3} {'R':>3} | {'H':>5} {'S':>5} {'V':>5}")
print("-" * 45)
for name, bgr in test_colors.items():
    pixel = np.uint8([[bgr]])
    hsv = cv2.cvtColor(pixel, cv2.COLOR_BGR2HSV)[0][0]
    print(f"{name:>4} | {bgr[0]:>3} {bgr[1]:>3} {bgr[2]:>3} | {hsv[0]:>5} {hsv[1]:>5} {hsv[2]:>5}")

色彩恒常性(Color Constancy)

人眼在不同光源下都能正确感知颜色,但相机不行。色彩恒常性算法校正这种偏差:

💻 色彩空间完整Pipeline代码

以下是一个完整的色彩空间处理管道,从数据准备到结果评估,包含所有关键步骤和参数调优建议:

import cv2
import numpy as np
import time

class 色彩空间Pipeline:
    """完整的色彩空间处理管道"""
    
    def __init__(self, params=None):
        self.params = params or {}
        self.results = {}
    
    def preprocess(self, image):
        """预处理步骤"""
        # 1. 去噪
        if self.params.get('denoise', True):
            image = cv2.GaussianBlur(image, (3, 3), 1.0)
        
        # 2. 对比度增强
        if self.params.get('enhance', False):
            if len(image.shape) == 2:
                image = cv2.equalizeHist(image)
            else:
                lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
                lab[:,:,0] = cv2.equalizeHist(lab[:,:,0])
                image = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
        
        # 3. 尺寸调整
        max_size = self.params.get('max_size', 1024)
        h, w = image.shape[:2]
        if max(h, w) > max_size:
            scale = max_size / max(h, w)
            image = cv2.resize(image, (int(w*scale), int(h*scale)))
        
        return image
    
    def process(self, image):
        """核心处理步骤"""
        t0 = time.time()
        preprocessed = self.preprocess(image)
        t1 = time.time()
        
        # 主处理逻辑
        result = self._main_process(preprocessed)
        t2 = time.time()
        
        self.results['timing'] = {
            'preprocess': (t1-t0)*1000,
            'process': (t2-t1)*1000,
            'total': (t2-t0)*1000
        }
        return result
    
    def _main_process(self, image):
        """主处理逻辑(子类可重写)"""
        return image
    
    def evaluate(self, result, ground_truth=None):
        """评估处理结果"""
        metrics = {}
        if ground_truth is not None:
            if len(result.shape) == 2:
                mse = np.mean((result.astype(float) - ground_truth.astype(float))**2)
                metrics['mse'] = mse
                metrics['psnr'] = 10 * np.log10(255**2 / (mse + 1e-10))
        metrics['timing'] = self.results.get('timing', {})
        return metrics
    
    def visualize(self, image, result):
        """可视化对比"""
        if len(image.shape) == 2:
            image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
        if len(result.shape) == 2:
            result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
        h1, w1 = image.shape[:2]
        h2, w2 = result.shape[:2]
        h, w = max(h1,h2), w1+w2+10
        vis = np.zeros((h, w, 3), dtype=np.uint8)
        vis[:h1, :w1] = image
        vis[:h2, w1+10:] = result
        cv2.putText(vis, 'Input', (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
        cv2.putText(vis, 'Output', (w1+20, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,255,0), 2)
        return vis


# 使用示例
pipeline = 色彩空间Pipeline({'denoise': True, 'enhance': False, 'max_size': 512})
test_img = np.random.randint(0, 256, (256, 256, 3), dtype=np.uint8)
result = pipeline.process(test_img)
metrics = pipeline.evaluate(result)
print(f"处理时间: {metrics['timing'].get('total', 0):.1f}ms")

🔧 参数调优指南

不同参数对结果的影响:

建议从默认参数开始,根据结果逐步调整。先在少量数据上快速迭代,确定参数后再全量处理。

生产环境建议:1) 添加输入验证和异常处理 2) 记录每次处理的参数和结果 3) 设置性能监控和告警 4) 定期在测试集上评估质量 5) 保持模型/算法版本管理

📊 完整评估指标

import numpy as np

class Metrics:
    @staticmethod
    def mse(img1, img2):
        return np.mean((img1.astype(float) - img2.astype(float))**2)
    
    @staticmethod
    def psnr(img1, img2, max_val=255):
        mse_val = Metrics.mse(img1, img2)
        if mse_val == 0: return float('inf')
        return 10 * np.log10(max_val**2 / mse_val)
    
    @staticmethod
    def iou(mask1, mask2):
        inter = np.logical_and(mask1 > 0, mask2 > 0).sum()
        union = np.logical_or(mask1 > 0, mask2 > 0).sum()
        return inter / (union + 1e-10)
    
    @staticmethod
    def f1_score(pred, gt, threshold=0.5):
        pred_bin = (pred > threshold)
        gt_bin = (gt > 0)
        tp = np.logical_and(pred_bin, gt_bin).sum()
        precision = tp / (pred_bin.sum() + 1e-10)
        recall = tp / (gt_bin.sum() + 1e-10)
        return 2 * precision * recall / (precision + recall + 1e-10)

# 示例
img1 = np.random.randint(0, 256, (100,100), dtype=np.uint8)
img2 = img1 + np.random.randint(-10, 10, (100,100))
img2 = np.clip(img2, 0, 255).astype(np.uint8)

print(f"MSE: {Metrics.mse(img1,img2):.2f}")
print(f"PSNR: {Metrics.psnr(img1,img2):.2f} dB")
print(f"说明: PSNR>30dB为良好, >40dB为优秀")

✅ 实机验证

🏆

色彩大师

你已经掌握了色彩空间的核心知识,继续前进!