目录
[五、颜色分割 + 轮廓检测,统计指定颜色物体数量](#五、颜色分割 + 轮廓检测,统计指定颜色物体数量)
[六、摄像头实时 HSV 颜色追踪](#六、摄像头实时 HSV 颜色追踪)
[七、HSV 滑动调参工具](#七、HSV 滑动调参工具)
[八、本章 API 总览表](#八、本章 API 总览表)
[十、方案选型:什么时候用灰度阈值,什么时候用 HSV 颜色分割](#十、方案选型:什么时候用灰度阈值,什么时候用 HSV 颜色分割)
上一章我们学习轮廓进阶全套能力,依靠灰度阈值、二值图像、轮廓检测完成物体识别计数。但是灰度阈值是依靠像素明暗区分前景背景,当画面光照复杂、物体和背景亮度接近的时候,灰度分割效果会很差。
这时可以使用HSV 颜色空间,依据色相(颜色本身)做分割,不受亮度干扰,可以直接把画面中指定颜色的物体提取出来。本章学习 BGR 与 HSV 互相转换、inRange 颜色阈值筛选、掩码 mask 位运算、颜色提取综合实战,包含静态图片处理以及摄像头实时颜色追踪案例。
OpenCV 读取图片默认是 BGR 通道,没有直接读取 HSV 格式图片的能力 ,必须先读取 BGR,再通过cv2.cvtColor()转换为 HSV。
一、BGR与HSV颜色空间理解
1.BGR空间
前面所有章节我们一直在使用BGR:由蓝、绿、红三个通道混合出色彩。
BGR的痛点:亮度变化会直接改变三个通道数值。同一个红色物体,强光下变亮、暗光下变暗,BGR三个数值波动巨大,很难固定一组数值筛选红色。
2.HSV三通道含义
HSV把颜色拆解为三个独立维度:
- H(Hue 色相):代表是什么颜色 。OpenCV中H取值范围:
0 ~ 179- 0左右:红色;30左右:黄色;60左右:绿色;120左右:蓝色;160左右:品红
- S(Saturation 饱和度):颜色鲜艳程度 ,取值
0 ~ 255。数值越高颜色越浓;S很低的时候颜色接近灰白。 - V(Value 明度/亮度):明暗程度 ,取值
0 ~ 255。控制像素亮暗,不受颜色种类影响。
核心优势:把「什么颜色」和「亮不亮」拆分开。筛选目标颜色主要依靠H,对光照强弱具备一定抗干扰能力。
转换函数:
python
# BGR → HSV
hsv_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2HSV)
# HSV → BGR
bgr_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
#BGR图像转HSV图像
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
cv2.imshow("BGR原图", img)
cv2.imshow("HSV图像", hsv)
while True:
key = cv2.waitKey(10)
if key == ord('q') or key == 27:
break
cv2.destroyAllWindows()
注意:直接imshow显示HSV图像,图片看起来会很奇怪。因为imshow依旧按照BGR规则解析矩阵,HSV图像仅用于计算,不要直接拿来观察色彩。
二、inRange函数:颜色区间筛选生成mask掩码
cv2.inRange(hsv图像, 下限lowerb, 上限upperb)
作用:设置HSV的最低、最高阈值,在hsv图里面把落在区间内的像素置255(白色),其余像素置0(黑色)。输出的结果叫做mask掩码(二值掩码图)。
参数格式:lowerb = np.array([H_min, S_min, V_min]);upperb = np.array([H_max, S_max, V_max])
掩码mask规则:
- 在HSV区间内 → mask该位置=255(白色,保留区域)
- 不在HSV区间内 → mask该位置=0(黑色,屏蔽区域)
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#示例:红色区间,根据实际图片微调数值
lower_red = np.array([0, 120, 80])
upper_red = np.array([10, 255, 255])
#生成掩码mask
mask = cv2.inRange(hsv, lower_red, upper_red)
cv2.imshow("原图", img)
cv2.imshow("mask掩码图", mask)
while True:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
输出mask是一张二值黑白图,白色代表满足该颜色的像素。
常见坑:H在OpenCV只有0‑179,不要直接使用PS的0‑360色相数值,需要除以2。
三、bitwise_and按掩码提取彩色目标
得到mask掩码之后,使用按位与cv2.bitwise_and(),利用掩码把原图中mask白色区域保留,黑色区域全部置黑。实现只把目标颜色物体抠出来。
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#红色阈值区间
lower_red = np.array([0, 120, 80])
upper_red = np.array([10, 255, 255])
mask = cv2.inRange(hsv, lower_red, upper_red)
#使用mask掩码,提取原图中目标颜色部分
result = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow("原图", img)
cv2.imshow("mask掩码", mask)
cv2.imshow("提取颜色结果", result)
while True:
key = cv2.waitKey(10)
if key == ord('q'):
break
cv2.destroyAllWindows()
常用基础位运算补充
| 函数 | 作用 |
|---|---|
cv2.bitwise_and(src1,src2,mask=mask) |
按位与,掩码抠图,保留mask白色对应区域 |
cv2.bitwise_or() |
按位或,多张掩码合并 |
cv2.bitwise_not(mask) |
图像取反,黑白互换 |
cv2.bitwise_xor() |
异或,取两张图差异部分 |
实操小技巧:inRange生成的mask往往带有细碎噪点,一般配合形态学操作(开运算闭运算)清理掩码里面的小白点、小黑洞。
四、预处理流程(颜色分割标准流水线)
原图 → BGR转HSV → inRange生成mask → 形态学操作清理mask噪点孔洞 → bitwise_and提取彩色目标 → 可选:对mask做轮廓检测计数
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower_red = np.array([0, 120, 80])
upper_red = np.array([10, 255, 255])
mask = cv2.inRange(hsv, lower_red, upper_red)
#形态学清理掩码噪点
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
mask_clean = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 去除外部噪点
mask_clean = cv2.morphologyEx(mask_clean, cv2.MORPH_CLOSE, kernel) # 填补内部孔洞
result = cv2.bitwise_and(img, img, mask=mask_clean)
cv2.imshow("原始mask",mask)
cv2.imshow("形态学清理后mask",mask_clean)
cv2.imshow("颜色提取结果",result)
cv2.waitKey(0)
cv2.destroyAllWindows()
五、颜色分割 + 轮廓检测,统计指定颜色物体数量
mask 清理干净之后,可以直接在 mask 上运行 findContours 轮廓检测,统计画面里面红色物体的个数。
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower_red = np.array([0, 120, 80])
upper_red = np.array([10, 255, 255])
mask = cv2.inRange(hsv, lower_red, upper_red)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))
mask_clean = cv2.morphologyEx(mask,cv2.MORPH_OPEN,kernel)
mask_clean = cv2.morphologyEx(mask_clean,cv2.MORPH_CLOSE,kernel)
#在清理完成的掩码图上查找轮廓
contours, hierarchy = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
draw_img = img.copy()
count = 0
min_area = 120
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
count +=1
x,y,w,h = cv2.boundingRect(cnt)
cv2.rectangle(draw_img,(x,y),(x+w,y+h),(0,255,0),2)
cv2.putText(draw_img,f"No.{count}",(x,y-8),cv2.FONT_HERSHEY_SIMPLEX,0.45,(0,0,255),1)
print(f"检测到目标颜色物体总数:{count}")
cv2.imshow("mask_clean",mask_clean)
cv2.imshow("物体标记结果",draw_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
六、摄像头实时 HSV 颜色追踪
读取摄像头每一帧,实时做 HSV 颜色分割,框选出画面内目标颜色物体。
python
import cv2
import numpy as np
vc = cv2.VideoCapture(0)
cv2.namedWindow("摄像头颜色追踪",cv2.WINDOW_NORMAL)
cv2.resizeWindow("摄像头颜色追踪",800,600)
#红色HSV阈值,可以根据实际效果修改
lower_red = np.array([0, 120, 80])
upper_red = np.array([10, 255, 255])
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))
min_area = 150
while vc.isOpened():
ret,frame = vc.read()
if not ret:
break
hsv = cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv,lower_red,upper_red)
#形态学清理mask
mask_clean = cv2.morphologyEx(mask,cv2.MORPH_OPEN,kernel)
mask_clean = cv2.morphologyEx(mask_clean,cv2.MORPH_CLOSE,kernel)
contours,hierarchy = cv2.findContours(mask_clean,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
draw_frame = frame.copy()
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area:
continue
x,y,w,h = cv2.boundingRect(cnt)
cv2.rectangle(draw_frame,(x,y),(x+w,y+h),(0,255,0),2)
#计算物体中心坐标,适配机器人颜色追踪需求
cx = x + w // 2
cy = y + h // 2
print(f"目标物体中心:cx={cx}, cy={cy}")
cv2.imshow("摄像头颜色追踪",draw_frame)
cv2.imshow("mask掩码",mask_clean)
key = cv2.waitKey(1000//24)
if key & 0xFF == ord('q'):
break
vc.release()
cv2.destroyAllWindows()
实操提示:摄像头环境光照变化大,如果识别不稳定,适当调小 S、V 下限,放宽阈值。
七、HSV 滑动调参工具
手动猜 HSV 数值很麻烦,可以创建滑动条,实时调节 H、S、V 上下限,观察 mask 效果,拿到适合自己图片的阈值。注意:本工具只针对单张静态图片调参,hsv 在循环外只计算一次,滑动条修改阈值时原图不会变化,这是正常现象,不要误以为它支持摄像头实时调参。
python
import cv2
import numpy as np
def callback(x):
pass
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
cv2.namedWindow("HSV调参窗口",cv2.WINDOW_NORMAL)
cv2.resizeWindow("HSV调参窗口",700,500)
#创建滑动条 H(0‑179) S、V(0‑255)
cv2.createTrackbar("H_min","HSV调参窗口",0,179,callback)
cv2.createTrackbar("H_max","HSV调参窗口",10,179,callback)
cv2.createTrackbar("S_min","HSV调参窗口",100,255,callback)
cv2.createTrackbar("S_max","HSV调参窗口",255,255,callback)
cv2.createTrackbar("V_min","HSV调参窗口",80,255,callback)
cv2.createTrackbar("V_max","HSV调参窗口",255,255,callback)
while True:
h_min = cv2.getTrackbarPos("H_min","HSV调参窗口")
h_max = cv2.getTrackbarPos("H_max","HSV调参窗口")
s_min = cv2.getTrackbarPos("S_min","HSV调参窗口")
s_max = cv2.getTrackbarPos("S_max","HSV调参窗口")
v_min = cv2.getTrackbarPos("V_min","HSV调参窗口")
v_max = cv2.getTrackbarPos("V_max","HSV调参窗口")
lower = np.array([h_min,s_min,v_min])
upper = np.array([h_max,s_max,v_max])
mask = cv2.inRange(hsv,lower,upper)
res = cv2.bitwise_and(img,img,mask=mask)
cv2.imshow("mask",mask)
cv2.imshow("HSV调参窗口",res)
key = cv2.waitKey(10)
if key == ord('q'):
# 打印最终阈值,可以复制到正式项目
print(f"lower = np.array([{h_min},{s_min},{v_min}])")
print(f"upper = np.array([{h_max},{s_max},{v_max}])")
break
cv2.destroyAllWindows()
八、本章 API 总览表
| 函数 | 功能说明 |
|---|---|
cv2.cvtColor(img,cv2.COLOR_BGR2HSV) |
BGR 图像转为 HSV 图像 |
cv2.inRange(hsv,lowerb,upperb) |
HSV 区间筛选,生成二值 mask 掩码 |
cv2.bitwise_and(img,img,mask=mask) |
根据掩码提取原图对应颜色区域 |
cv2.bitwise_not(mask) |
掩码黑白取反 |
cv2.createTrackbar() |
创建滑动条,用于调试 HSV 阈值 |
九、本章高频踩坑清单
红色色相跨 0 边界,需要两组 inRange 掩码做 or 合并,完整示例:
python
import cv2
import numpy as np
img = cv2.imread(r"D:\PYTHON\color_test.jpg")
if img is None:
print("图片读取失败,请替换为正确路径")
exit()
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#红色跨边界:H 0-10 和 160-179 两组区间
lower_red1 = np.array([0, 120, 80])
upper_red1 = np.array([10, 255, 255])
lower_red2 = np.array([160, 120, 80])
upper_red2 = np.array([179, 255, 255])
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = cv2.bitwise_or(mask1, mask2) # 两组掩码合并
result = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow("mask", mask)
cv2.imshow("result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
- OpenCV 中 Hue 色相取值范围0‑179,不是 0‑360。PS、画图软件的色相数值要除以 2 再使用;
- 不要直接 imshow 显示 HSV 图像,HSV 矩阵只是计算用,imshow 会错误解析色彩;
- inRange 输出 mask 会存在噪点,务必搭配形态学开运算、闭运算清理掩码;
- S 饱和度不要设置太低:S 过低时物体发白、发灰白,H 色相会失效,一般 S_min 建议不低于 40~60;
- V 明度要根据光照环境调整:强光环境适当拉高 V_max,暗光环境适当降低 V_min,避免目标物体因过亮或过暗被漏掉;
- 图片路径使用 r 原始字符串,避免转义字符导致图片读取返回 None;
- 摄像头实时颜色追踪,光照变化会严重影响分割效果,尽量保证光源稳定。
十、方案选型:什么时候用灰度阈值,什么时候用 HSV 颜色分割
学完本章,很多人会纠结:到底该用灰度阈值还是 HSV 颜色分割?其实两者并不冲突,选型主要看两个问题:物体和背景靠什么区分 ,以及光照环境是否稳定。
- 物体和背景明暗差距大,颜色不重要:优先用灰度 + threshold。例如白纸黑字、深色零件放在浅色传送带上,这类场景灰度阈值简单高效,不需要转 HSV,处理速度也更快。
- 物体依靠颜色区分,或光照变化大:优先用 HSV + inRange。例如红色球在绿色草地上、彩色积木分类,这类场景颜色是核心特征,HSV 依靠色相筛选,对光照强弱具备一定抗干扰能力。
简单记忆:看明暗选灰度,看颜色选 HSV。实际项目中两者也经常配合使用,例如先用 HSV 提取目标颜色区域,再对结果做灰度阈值或轮廓检测,完成更复杂的识别任务。
十一、本章知识小结
- BGR 色彩空间容易受亮度干扰;HSV 把图像拆分为 H 色相、S 饱和度、V 明度,依靠色相筛选颜色,抗光照干扰更强;
- 使用
cv2.cvtColor()完成 BGR 与 HSV 互相转换; cv2.inRange()设置 HSV 上下限,生成 mask 掩码,白色代表符合颜色条件的像素;- bitwise_and 位运算配合 mask 掩码,从原图抠取目标颜色区域;
- mask 掩码一般要经过形态学操作,去除噪点、填补孔洞;mask 可以直接送入 findContours 做轮廓检测,实现颜色物体计数;
- inRange 输出的 mask 本身就是 uint8 单通道二值图,可以直接送入 findContours,不需要再做 threshold 二值化;
- 滑动条工具可以快速调试 HSV 阈值,拿到适合自己场景的参数;
- 红色比较特殊色相跨过 0 边界,复杂场景需要两组掩码合并。
下一章:OpenCV 仿射变换、透视变换,图像任意角度旋转矫正,实现文档扫描矫正实战。