Linux下OpenCV实战:cvtColor颜色转换与putText图像标注详解
在Linux下做图像处理很多朋友一上来就直奔深度学习、模型部署那一套但真到调试阶段会发现天天陪你加班的反而是几个最不起眼的基础函数。cvtColor和putText就是典型代表一个负责把图像在BGR、灰度、HSV这些颜色空间之间来回切换另一个负责把识别结果、坐标、置信度这些信息直观地画到画面里。坦白讲最开始我也觉得这两个函数简单到不用学直到做嵌入式Linux视觉项目时连踩了几个跟头才老老实实回去把文档和源码翻了底朝天。这期就围绕这两个函数把原理、参数、实操坑位和一套可以直接拿去用的代码整理完整适合刚开始在Linux上写OpenCV的朋友也适合做检测识别项目、需要快速验证和结果可视化的同学。1. 环境准备与开发语言选型先别急着敲代码把环境理顺能省下一大半的折腾时间。很多初学者在Linux上装OpenCV时被一堆依赖吓得直接弃坑其实现在的安装路径比前几年友好太多。1.1 三种安装方式对比我平时在Debian系发行版包括各类以Debian为基础的桌面系统上推荐三条路按场景选就行# 方式一Python pip最快粗暴 pip install opencv-python opencv-contrib-python # 方式二系统Python包apt伺候 sudo apt update sudo apt install python3-opencv # 方式三C开发库写工程用的 sudo apt install libopencv-dev网上很多教程上来就让你源码编译这个我一般劝退。OpenCV整套编下来二三十分钟起步内存小的机器能卡到怀疑人生。源码编译真正该上场的场景是需要CUDA加速、要裁剪编译模块、或者给ARM板子做交叉编译。普通PC上做学习验证、跑算法实验直接用预编译包完全够用没必要自讨苦吃。安装方式优点缺点最合适场景pip安装快、版本常更新可能与系统包冲突Python快速验证、深度学习项目apt安装与系统集成到位版本偏旧桌面系统、长期稳定环境源码编译模块可裁剪、可加速耗时、依赖又多又碎嵌入式交叉编译、GPU定制场景装完后记得验证一下免得后面写半天发现根本没装上python3 -c import cv2; print(cv2.__version__) # C环境检查 pkg-config --modversion opencv4如果pkg-config提示找不到opencv4试一下把包名改成opencv。有些发行版里还是用的旧命名这条我在Ubuntu 20.04和Debian 11上都踩过。1.2 Python和C到底怎么选这问题几乎每个来问我的新人都会纠结。我的回答就一句验证思路和跑测试用Python落地工程和上嵌入式用C。Python绑定的函数签名基本就是C接口照搬过来的所以拿Python先摸清参数行为切到C时几乎零成本。而且Python获取图像方便配合Jupyter这类工具中间结果随时print、随时看那个调试效率是C没法比的。真正到了嵌入式Linux场景比如主控板、工业相机流水线、边缘盒子C的优势就显出来了没有Python解释器开销内存占用可控启动速度快。我见过不少初学者一上来就用Python写生产级视觉服务结果内存一涨就崩后来换C重写一遍才彻底稳。不过这里的重写其实不痛苦因为核心逻辑就是那几个API调用换个语言外壳而已。2. cvtColor颜色空间转换的底层逻辑与高频用法cvtColor是OpenCV里出场率最高的函数之一但大多数人直接用默认参数出了奇怪现象又不知道从哪排查。这里把原理拆开聊聊。2.1 一个反直觉的设定OpenCV为什么默认是BGR初学OpenCV的人都会撞见同一个诡异现象用cv2.imread()读进来的图如果用matplotlib的plt.imshow()直接显示画面里红蓝完全颠倒。之所以这样是因为OpenCV默认用BGR顺序存储图像而matplotlib和绝大多数图像软件按RGB处理。这个“反直觉”是有历史原因的。早期摄像头传感器输出的原始数据顺序就倾向于BGROpenCV又是面向摄像头采集起家的于是顺手把这个顺序固化成了默认值。到今天我们不管用什么硬件读出BGR图像已经成了一种事实标准。想要把OpenCV图像给其他库用就靠cvtColor倒腾一次import cv2 img cv2.imread(demo.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB)我见过有人因为不知道这回事在图像预处理里来回转两三次结果颜色越来越怪。真正理解BGR这个默认值之后很多调色异常的问题都能一眼定位。2.2 cvtColor函数签名拆解函数签名在Python和C里就差一个可变参数的写法核心就这几个参数cv2.cvtColor(src, code, dstNone, dstCn0)src输入图像常见的是8位三通道CV_8UC3。code转换代码决定输出图像的通道数和计算方式。dst输出图像Python里一般不手动分配。dstCn目标通道数默认0表示由code自动决定。code这个参数是核心。我最常用的是这几组COLOR_BGR2RGB # 通道重排BGR转RGB COLOR_BGR2GRAY # 转灰度三通道变单通道 COLOR_BGR2HSV # 转HSV做颜色提取 COLOR_HSV2BGR # HSV转回BGR方便显示 COLOR_GRAY2BGR # 单通道复制成三通道做叠加显示用一个初学者特别容易犯的问题不确认图像当前到底是什么格式上来就乱选code。我自己的排查顺序很固定先print(img.shape)看是几通道、分辨率多少再确认读入时有没有返回None最后才去检查code。顺序反了容易白忙活一场。2.3 灰度转换与HSV空间以及那个经典的H范围坑灰度转换的本质是加权平均常见公式为gray 0.299R 0.587G 0.114BOpenCV内部在integer实现里用的系数略有出入但权重思路完全一致。这组权重对应的是人眼对绿色最敏感、对蓝色最不敏感的生理特征。所以灰度图看着“舒服”并不是玄学而是它本来就在模拟人眼的亮度感知。HSV空间则是另一套逻辑把颜色拆成色相Hue、饱和度Saturation、明度Value。做图像分割时HSV比RGB直观得多——RGB里判断“红色”需要同时约束三通道的比值关系一个光照变化就能让像素点在RGB空间里飘很远而HSV直接按H划范围S和V控制筛选粗细抗光照变化能力强不少。这里有个特别容易踩的坑OpenCV中8位图像的H范围不是0-360而是0-179。S和V倒还是0-255。H被压缩的原因很实在8位图像单通道最多只存0到255360种色相塞不进去于是OpenCV做了个折中用180份去表示一圈色相。如果你照搬其他图像库里“红色H是0到10以及160到180”的说法在OpenCV里直接用是没问题的。但如果你习惯360度范围把30度的红色写成H30来做阈值那提取结果就是全黑。这个坑我见过无数人掉进去自己当初也没幸免调了一下午才发现问题。2.4 调试技巧中间结果可视化很多人做完cvtColor之后直接把结果扔给imshow或imwrite发现输出和预期不一样就开始瞎猜。其实中间过程一眼就能看出来# 用hconcat把转换前后的图并排放 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) show cv2.hconcat([img, cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR), cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)]) cv2.imwrite(compare.jpg, show)注意HSV图不能直接imshow看因为HSV的数值语义和BGR完全不一样硬看会以为是坏图。必须转回BGR再显示。这个习惯养成了cvtColor相关的浪费型排错能减少一大半。3. putText在图像上写字的技术细节与排版putText看着简单真想排版排得漂亮也是门手艺。坐标基准、字体缩放、基线概念每一个都能让新手栽跟头。3.1 putText参数逐个说cv2.putText(img, text, org, fontFace, fontScale, color, thickness1, lineTypecv2.LINE_8, bottomLeftOriginFalse)img目标图像。text要写的文本内容。org文本左下角起点坐标不是左上角这个最容易被当成左上角导致文字整体偏低。fontFace字体。常用FONT_HERSHEY_SIMPLEX这个最稳。fontScale字体放大比例不是具体像素大小。colorBGR颜色注意顺序。thickness笔画粗细。常用1或2过大的值连笔画一起糊掉。lineTypeLINE_AA抗锯齿更好看LINE_8快一点点。字体选项里有FONT_HERSHEY_SCRIPT_SIMPLEX这种长得像手写体的选项中看不中用还容易令人误解我建议普通需求死磕SIMPLEX就够了。putText坐标系统里还有个bottomLeftOrigin参数默认False表示文本的正向朝上。这个参数绝大多数时候用默认值只有做特殊标注时可能启用到。3.2 文本居中的计算公式让我来把这个实操细节摊开讲。想在图像中央写字很多人的第一直觉是org (img_w // 2, img_h // 2)然后写出来的文本整体偏右下怎么调都差口气。原因是putText拿左下角当锚点而不是以文本框中心做锚点。正确做法先用getTextSize取文本尺寸再按坐标系换算(text_w, text_h), baseline cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2) org_x (img_w - text_w) // 2 org_y (img_h text_h - baseline) // 2 cv2.putText(img, text, (org_x, org_y), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2)baseline是文本底部基线下方预留的高度。很多人忽略它对垂直居中的影响直接写(img_h text_h) // 2文字视觉上就会向下偏移。这个细节在标框、标条线时特别明显。我的习惯是封装一个小函数专门做居中文本以后不管给哪张图加字都一行调用def draw_center_text(img, text): text_h, baseline cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)[:2] x (img.shape[1] - text_h[0]) // 2 if False else (img.shape[1] - cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)[0][0]) // 2 y (img.shape[0] cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)[0][1] - cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)[1]) // 2 cv2.putText(img, text, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2)这段代码略啰嗦但意思很明白一切尺寸以getTextSize的返回为准别自己拍脑袋算。3.3 在Linux上显示中文的坑这是LinuxOpenCV槽点最密集的地方没有之一。OpenCV内置的Hershey字体是上世纪70年代的矢量字体只覆盖ASCII字符集遇到中文直接画个问号。Windows下某些版本能调用系统字体多多少少能显示中文一到Linux就彻底原形毕露。解决方案其实有三个思路按工程性质灵活选思路一PIL/Pillow绕道from PIL import Image, ImageDraw, ImageFont import numpy as np import cv2 img_bgr cv2.imread(demo.jpg) img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(img_rgb) draw ImageDraw.Draw(pil_img) # 路径按你系统实际字体来 font ImageFont.truetype(/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc, 24) draw.text((10, 10), 你好OpenCV, fontfont, fill(255, 255, 255)) img_bgr cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)这个方案部署成本低唯一要求是系统里得有中文字体文件。很多Linux精简版系统默认没装中文字体需要apt install fonts-wqy-zenhei。思路二用opencv-contrib的freetype模块这是个纯OpenCV路线但通常需要自己编译带contrib的OpenCV对非编译爱好者不友好。好处是一旦编译好API风格和putText几乎一样。如果你正巧在源码编译这个模块值得加进去。思路三提前渲染中文标签图运行时叠加这个在检测项目里更实用。比如要标“行人”“车辆”这类固定中文词可以先离线渲染成透明背景的小图运行时用带透明通道的图像叠加到目标框上方。这样避免了每次绘制都调用重型的图像处理库省CPU显示效果还可以很精致。工程上我一般优先思路三因为检测场景里中文标签都是固定集合预渲染省心又省力开发调试阶段用思路一灵活变更文本内容很方便。4. 实操一个完整的颜色检测与标注小程序理论知识说再多不如跑一遍实际项目。下面这个例子是我做视觉调试时常用的骨架读入图像、转HSV、提取红色目标、绘制检测框、用putText标注面积信息和坐标最后保存结果图。4.1 需求拆解假设我们拿到一张工业流水线上的画面里面有几个红色物体。任务很明确判断哪些区域是红色的用矩形框把它们框出来并且在框旁边写清楚面积和坐标。这几乎覆盖了cvtColor和putText的日常用法转颜色空间、做阈值提取、可视化标注。4.2 完整代码与逐段解析import cv2 import numpy as np # 1. 读入图像顺便转成合适尺寸 img cv2.imread(demo.jpg) if img is None: raise ValueError(图像路径不对或者文件损坏) img cv2.resize(img, (640, 480)) # 2. 颜色空间转换灰度与HSV gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 3. 红色范围在HSV里的两个区间 # 因为红色横跨0和180的首尾一个区间包不住 lower_red1 np.array([0, 100, 100]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([160, 100, 100]) upper_red2 np.array([180, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) # 4. 找轮廓框出目标 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 500: continue # 过滤掉噪点小目标 x, y, w, h cv2.boundingRect(cnt) cv2.rectangle(img, (x, y), (x w, y h), (0, 0, 255), 2) label fArea:{int(area)} ({x},{y}) # 先算文本宽高再画背景条和前景字 (text_w, text_h), baseline cv2.getTextSize( label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1) # 在目标框上方画一个实填充的黑色背景防止文字被图像内容干扰 cv2.rectangle(img, (x, y - text_h - baseline - 6), (x text_w, y), (0, 0, 0), -1) # 再画文字颜色用白色坐标按背景条内部微调 cv2.putText(img, label, (x, y - baseline - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) # 5. 保存结果 cv2.imwrite(output.jpg, img) print(saved output.jpg)第3步里那个红色范围双区间特别重要。因为HSV的H轴是环形的红色既分布在接近0的区域也分布在接近180的区域单区间根本盖不住。很多人在“为什么我的红色目标提取不全”这个问题上卡住就是没意识到H轴是个环。第4步里先把背景条画深色再往上面写白色文字这个“背景条文字”的组合在实际工程里非常常用。否则图像内容复杂度一高白色的字就直接融进背景里看不清。4.3 运行步骤与结果验证保存为demo.py后直接终端里跑python3 demo.py跑完会生成output.jpg。打开看几个点红色目标是否都被框出来了框是否紧贴物体边缘。标注文字是否清晰坐标位置是否符合预期。有没有零散小框如果很多小框说明面积阈值500太小调大即可。这套流程同时也适用于视频帧处理——把cv2.imread换成VideoCapture读入的一帧逻辑完全一样。如果想把这段逻辑用到C工程中我给出一个直接的编译命令参考g demo.cpp -o demo $(pkg-config --cflags --libs opencv4)pkg-config会帮你自动补齐头文件路径和动态库链接路径不用手动一行行列-l参数。只要libopencv-dev装好了这个命令基本一击即中。5. 常见问题与排查实录最后把这些年遇到的典型问题整理成一份速查册覆盖安装、运行、显示三个阶段的常见坑。5.1 安装与编译阶段问题1pip安装后import cv2报段错误这个多半是你已经装了apt版本的OpenCVpyenv/conda又装了另一套两套库混在一起打架。解决办法统一来源优先走pip卸载apt版本或用虚拟环境隔离开。问题2C工程里undefined reference to cv::cvtColor链接库没找全。用pkg-config --cflags --libs opencv4把编译参数带上或者手动在CMakeLists.txt里find_package(OpenCV REQUIRED)。千万别只用-lopencv_core然后指望函数都在core里这是很多新手想当然犯的错。5.2 运行与逻辑阶段问题3灰度转换后整个画面偏黑先print一下输入图像的数据类型和通道数。正常读入的JPEG是uint8三通道但如果你的图像本身是单通道再用COLOR_BGR2GRAY就会得到一个意外结果。这种情况用COLOR_GRAY2GRAY也是无害的。关键还是先确认输入。问题4putText写字写到了图像外面没有先getTextSize就按直觉坐标写。解决办法就是我在3.2里给的那套居中逻辑要么把坐标框到范围内再写要么根据文本尺寸动态计算。检测框标注场景里如果标注目标太靠近图像边缘还要考虑把文字改放到目标框的下方或者内部。问题5中文全变成问号首先确认这不是putText的锅OpenCV默认字体就不支持中文。参考3.3的PIL绕道或预渲染方案不要指望改参数能解决。问题6HSV提取目标混乱轮廓一堆噪声先检查S和V的下限是不是设得太低。光照不足时低饱和度像素容易被算进目标区域产生大量噪声。调参时优先固定H范围再慢慢抬高S和V下限同时配合形态学开闭运算把零散噪声去掉会比单纯加大面积阈值更有效。5.3 经验速查表现象根本原因解决方案图像显示蓝红颠倒BGR和RGB未转换显示前cvtColor转RGB或保存用BGRcvtColor结果全黑输入数据类型/通道与code不符先print图像shape和dtype再选codeHSV提取不到目标H范围超出0-179检查H值范围超过上限必然出黑图红色目标提取不全红色横跨H轴首尾用两个区间分别提取再mergeputText文字跑到图外没用getTextSize计算尺寸先算文本框动态调整org坐标中文在Linux下变问号OpenCV字体仅ASCIIPIL绕道或预渲染中文标签图findContours报错参数不足OpenCV新老版本返回值不同按版本解包老版3个返回新版2个返回我个人实际开发中还有一个心得把cvtColor和putText放在一个工具函数库里统一封装。每次需要转换颜色空间、画标注时都走封装好的函数而不是临时敲参数。因为这两个函数参数太多稍不注意就会搞混BGR顺序、H范围、字体缩放这些细节封装起来就能减少出错。并且调试时统一打印中间结果的shape也能防止很多“莫名其妙”的异常。如果你也是刚在Linux上玩OpenCV建议先把第4节的例子完整跑一遍再结合自己的场景替换成特定颜色的提取。等跑顺了把思路迁移到视频流或摄像头画面上你会发现QT和显示器的图形化调试瞬间不香了直接在图像上标注关键信息反而更高效。后面有机会我再聊聊轮廓检测和视频处理这两个主题通常和cvtColor、putText是连着一起用的。