当你浏览电商平台时,有没有注意过那些根据颜色分类的商品?比如"红色连衣裙"、"蓝色运动鞋"这类标签。这些颜色标签很多都是通过算法自动识别的。我在做一个服装推荐系统时就遇到过这个问题:如何让程序"看懂"图片的主色调?
传统方法是通过人工标注,但面对海量图片时效率太低。后来我发现用OpenCV分析HSV颜色空间的直方图是个很实用的解决方案。HSV(Hue色相、Saturation饱和度、Value明度)比RGB更接近人类对颜色的感知方式。比如在HSV空间里,不同深浅的红色都会归到相近的色相值,而在RGB空间里可能相差很远。
举个例子,去年帮朋友优化他的摄影作品展示网站时,我们实现了按色调自动分类功能。用户上传照片后,系统会自动提取主色调并打上标签。实测下来,用HSV直方图分析的方法准确率能达到85%以上,特别是对色彩集中的产品图效果更好。
HSV是个圆柱坐标系模型,可以想象成一个色轮:
为什么用HSV而不是RGB?看这个实际案例:要识别"红色"的商品图。在RGB空间,深红(150,0,0)和浅红(255,100,100)的数值差异很大。但在HSV空间,它们的H值都接近0,只是S和V不同。这就大大简化了颜色识别。
我常用的HSV颜色分段如下:
python复制hsv_ranges = {
"红色": [(0,10),(156,180)], # 红色在色轮两端
"橙色": (11,25),
"黄色": (26,34),
"绿色": (35,77),
"青色": (78,99),
"蓝色": (100,124),
"紫色": (125,155),
"黑白灰": [(0,180),(0,255),(0,255)] # 需要特殊处理
}
直方图就是统计每个区间的像素数量。比如我们把H分成8个区间,统计每个区间有多少像素。主色调就是像素最多的那个区间。
实际操作时要注意:
python复制import cv2
import numpy as np
img = cv2.imread('product.jpg')
img = cv2.GaussianBlur(img, (5,5), 0)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 取中心区域
h,w = hsv.shape[:2]
roi = hsv[int(h*0.1):int(h*0.9), int(w*0.1):int(w*0.9)]
# 计算直方图
h_hist = cv2.calcHist([roi], [0], None, [8], [0,180])
s_hist = cv2.calcHist([roi], [1], None, [3], [0,256])
v_hist = cv2.calcHist([roi], [2], None, [3], [0,256])
处理电商图片时,背景往往是纯色。我建议:
python复制def get_main_object(img):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return img
max_contour = max(contours, key=cv2.contourArea)
x,y,w,h = cv2.boundingRect(max_contour)
return img[y:y+h, x:x+w]
我的经验是综合三个指标:
python复制def get_dominant_color(hsv_img):
# 计算各通道直方图
h_hist = cv2.calcHist([hsv_img], [0], None, [12], [0,180])
s_hist = cv2.calcHist([hsv_img], [1], None, [4], [0,256])
v_hist = cv2.calcHist([hsv_img], [2], None, [4], [0,256])
# 找出H主峰值
h_main = np.argmax(h_hist)
h_value = h_main * 15 + 7.5 # 转换为中间值
# 计算S加权平均
s_weights = np.array([0.2, 0.5, 0.8, 1.0]) # 饱和度越高权重越大
s_avg = np.sum(s_hist.flatten() * s_weights) / np.sum(s_hist)
# 计算V加权平均
v_weights = np.array([0.1, 0.3, 0.7, 1.0]) # 避免太暗
v_avg = np.sum(v_hist.flatten() * v_weights) / np.sum(v_hist)
return h_value, s_avg, v_avg
根据HSV值判断具体颜色名称时,要注意边界情况:
python复制def classify_color(h, s, v):
if v < 50:
return "黑色"
if s < 50 and v > 200:
return "白色"
if s < 50:
return "灰色"
if (0 <= h < 10) or (170 <= h <= 180):
return "红色"
elif 10 <= h < 25:
return "橙色"
elif 25 <= h < 35:
return "黄色"
elif 35 <= h < 80:
return "绿色"
elif 80 <= h < 100:
return "青色"
elif 100 <= h < 125:
return "蓝色"
else:
return "紫色"
遇到背景杂乱的图片时,我通常这样做:
python复制def kmeans_dominant_color(img, k=3):
pixels = img.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)
# 统计每个聚类的像素数
counts = np.bincount(labels.flatten())
# 按大小排序
sorted_idx = np.argsort(counts)[::-1]
dominant_colors = []
for i in sorted_idx[:2]: # 取前两个主要颜色
dominant_colors.append(centers[i])
return dominant_colors
处理大量图片时,这些优化很有效:
python复制def fast_dominant_color(img):
# 缩小尺寸
small_img = cv2.resize(img, (0,0), fx=0.25, fy=0.25)
# 转为HSV
hsv = cv2.cvtColor(small_img, cv2.COLOR_BGR2HSV)
# 只取H通道
h = hsv[:,:,0]
# 简化直方图计算
hist = cv2.calcHist([h], [0], None, [12], [0,180])
return np.argmax(hist) * 15
我在项目中遇到的几个典型问题:
python复制def white_balance(img):
result = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
avg_a = np.mean(result[:,:,1])
avg_b = np.mean(result[:,:,2])
result[:,:,1] = result[:,:,1] - ((avg_a - 128) * (result[:,:,0] / 255.0) * 1.1)
result[:,:,2] = result[:,:,2] - ((avg_b - 128) * (result[:,:,0] / 255.0) * 1.1)
return cv2.cvtColor(result, cv2.COLOR_LAB2BGR)