ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenCV画图函数全解析:从line、circle到中文标注与实战避坑指南

OpenCV画图函数全解析:从line、circle到中文标注与实战避坑指南

1. 画图函数速览:为什么OpenCV内置的画图能力这么重要

很多人一听到OpenCV,第一反应就是图像处理、人脸识别、目标检测这些“高大上”的功能,反而把最基础、最常用的画图函数给忽略了。实际上,line、circle、rectangle这些函数几乎是每个OpenCV项目都跑不掉的——你在调试目标检测算法时,需要在图上画出检测框看看效果;你做图像标注工具时,需要手动框选区域;你做摄像头实时预览时,需要绘制FPS、提示文字;甚至你做轮廓分析时,要用polylines把结果可视化出来。可以说,画图函数是贯穿整个图像处理流程的基础工具,也是最能直观反馈算法效果的核心手段。

这篇文章就把OpenCV里几个最常用的画图函数一次性讲透——line、circle、rectangle、ellipse、polylines、putText,以及它们背后的Rect类。我会结合自己实际写代码时踩过的坑和总结的经验,把每个函数的参数含义、使用逻辑、常见问题都拆开来讲,配合可直接复现的代码示例,保证你看完就能直接在项目里用起来。

这篇文章的内容适合这些读者:刚入手OpenCV、被官方文档里一堆参数搞得头大的新手;写了好一阵子代码但总在画图细节上报错的中级开发者;还有需要做图像标注、算法可视化的工程实践者。内容从最基础的概念讲起,但也会涉及一些进阶用法和避坑经验,就算你已经用过这些函数,大概率也能找到一两条值得收藏的技巧。

开讲之前先给个目录,方便你按需跳读:

  • 画图前的预备知识:图像坐标系、颜色格式这些容易搞错的基础概念
  • 逐个函数深度拆解:line、circle、rectangle、ellipse、polylines、putText
  • Rect类的使用技巧:它和rectangle函数到底是什么关系
  • 综合实战:一个完整的图像标注Demo,把函数串起来用
  • 典型问题排查:我遇到过的报错和解决方案,整理成速查表

2. 画图前的三个基础概念:坐标系、颜色格式、画布类型

2.1 图像坐标系:原点在左上角,x向右,y向下

这个点看起来太简单了,但真有人在这里栽跟头。OpenCV里图像坐标系的定义是:原点在图像左上角,x轴水平向右,y轴垂直向下。也就是说,像素坐标(x, y)的x对应列(width方向),y对应行(height方向)。这和数学里的坐标系不一样,数学通常y轴向上,所以画一条从(0,0)到(100,100)的直线时,它是一条从左上角向右下角的斜线,而不是你直觉中的“从左下到右上”。

我在用OpenCV做定位项目时,一开始也经常把坐标写反——想画一个左上角的矩形,结果画到了左下角。后来养成了一个习惯:拿到一张图的尺寸后,先看一眼shape再动手,确认一下height在前还是width在前。如果你用numpy直接创建一个画布,shape的格式是(height, width, channels),而画图函数里的坐标全是(x, y)也就是(width, height),这是一个非常容易混淆的点,务必牢记。

2.2 颜色三通道:OpenCV用的是BGR,不是RGB

第二个基础坑就是颜色通道顺序。OpenCV读入图片时,默认颜色空间是BGR——蓝色、绿色、红色,和我们通常说的RGB完全相反。如果你直接创建一个颜色元组然后赋值给画图函数,一不留神就会“互换”红蓝通道。

我举个例子:你想画一个红色的圆,直觉上会写(255, 0, 0),但在OpenCV里这是纯蓝色。正确写法应该是(0, 0, 255)。这个细节如果只是画图可能影响不大,但你在做图像标注、可视化目标框时,颜色搞错会直接影响你判断结果。而且当你从某个网页教程里复制代码时,很多网页贴的代码本身颜色就是错的,运行出来跟想象完全不一样,问题还不容易排查。

我不止一次在工程项目中看到这样的问题。有次做一个缺陷检测的项目,同事用OpenCV标出来的“红色”缺陷区域,实际上是蓝色的,供应商看了界面后一直问为什么缺陷区域用蓝色标记。原因就是这么一条通道顺序。所以这里有个小建议:如果你拿到一只BGR像素,想正确显示到屏幕上,要么用cv2.cvtColor转成RGB,要么在传参时手动调换通道顺序。Windows上如果用OpenCV自带窗口显示,BGR和RGB的差异会自动转换,但在网页输出或保存文件时就会暴露。

2.3 画布类型:单通道、三通道、八位无符号整型

OpenCV的画图函数兼容多种Mat类型,但绝大多数情况我们用三通道彩色图或者单通道灰度图。画图结果看得清楚的前提是:图像数据必须是8位无符号整型(uint8),这个类型每个通道的取值范围是0到255。如果你用浮点型的图直接去画,往往只会得到一团灰或一团黑。更稳妥的做法是先用numpy创建一张纯色背景画布,用0填充就是全黑,用255填充就是全白。

颜色、线宽、坐标这几个参数其实还有一个隐藏的类型要求。OpenCV在C++里区分了多种数值类型,但在Python封装里它已经帮你做了类型推断。唯一需要注意的是,坐标必须是整数或者能通过int()安全转换的数值,你不能直接传一个浮点坐标进去。比如line一个从(10.5, 20)到(30,40)的直线,在Python版OpenCV里大概率会报错或者忽略小数部分,所以我通常在画图前会对坐标做一次int()转换。

我自己最常用的创建画布方式是这样的:

import cv2 import numpy as np # 创建一个全黑的三通道画布,高度480,宽度640 canvas = np.zeros((480, 640, 3), dtype=np.uint8) # 创建一个全白的画布 canvas_white = np.ones((480, 640, 3), dtype=np.uint8) * 255

用numpy创建的好处是灵活,而且后续做图像叠加、掩模操作也都顺手。如果你只是单纯想在原图或读取的图片上画图,那直接用imread回来的Mat即可。

3. line函数深度拆解:参数细节与三类场景

3.1 参数逐项解读

cv2.line的画法其实很简单,官方签名长这样:

cv2.line(img, pt1, pt2, color, thickness=1, lineType=cv2.LINE_8, shift=0)
  • img:要画线的图像对象,直接原地修改,不需要接收返回值
  • pt1、pt2:线段的起点和终点坐标
  • color:颜色元组(B, G, R)
  • thickness:线宽,单位是像素,如果传-1就是填充整个形状(后面会提到)
  • lineType:线的类型,常见LINE_4、LINE_8、LINE_AA,分别代表4邻接、8邻接、抗锯齿
  • shift:坐标点的小数点位数,绝大多数场景我们用不到,默认0就行

这里有个关键理解:cv2.line画的是线段,不是无限延伸的直线。如果起点和终点是同一个点,它不会画出任何东西。很多刚上手的朋友以为line能画向量直线,结果画了半天发现是一条短促的线段,误以为代码有问题,其实是对函数行为预期有偏差。

3.2 画箭头和网格:line的辅助技巧

line函数本身只画线段,但你可以通过一点数学变换快速画出箭头、网格、十字线这些常用图形。十字线是很多图像预览界面里的标配,画法就是横竖各一条线:竖直方向从图像中线画到两端,水平方向同理。箭头则需要你在线段终点处根据方向角再加两条小斜线。如果你需要频繁画箭头,可以直接用OpenCV自带的cv2.arrowedLine函数,但了解line的原理有助于自己控制箭头样式。

网格在图像标注、棋盘格标定中经常需要。实现思路就是循环遍历每条线的起点和终点,线的间距控制好就行。我实际写代码时会先把画布宽高取出来,再用for循环画出等间距交叉的横竖线,效果干净利落,比手动一个个画要稳得多。

3.3 注意事项

  • line函数原地修改图像,所以如果你需要保留原图副本,记得先深拷贝:img.copy(),否则画完图原图也被污染了
  • 坐标越界不会报错,OpenCV会自动裁剪到画布范围内,但这个“越界不报错”的特性也可能掩盖你逻辑上的坐标错误,调试时最好自己加断言
  • thickness=0不会画线,这个值不会被视为“最细线”,最细的线是thickness=1
  • 如果你画了很多条线之后发现线条比较模糊,检查一下是不是用了LINE_AA(抗锯齿)后的视觉效果差异,普通显示场景LINE_8就够了,只有在需要精确像素粒度时才适合用LINE_AA

4. circle、rectangle、ellipse:常见的几何图形怎么画才不踩坑

4.1 circle函数:从画圆到实心圆

circle的参数比line稍多一点:

cv2.circle(img, center, radius, color, thickness=1, lineType=cv2.LINE_8, shift=0)
  • center:圆心坐标(x, y)
  • radius:半径(整数)
  • thickness:线宽,关键点是传-1时画实心圆

很多看过官方文档的人都会有疑问:thickness=-1为什么是实心而不是报错?这是OpenCV里的统一约定:线宽设为-1代表用当前颜色填充该形状。line、circle、rectangle、ellipse、polylines这几个函数都有这个约定。我经常用这个特性来画实心圆作为标记点,比如在目标检测或关键点检测结果可视化时,用一个小实心圆标出关键点位置,比空心圆更醒目。

另外提一句:radius如果给的太大导致圆超出图像边界,OpenCV会做裁剪,不报错。但如果radius给的是负数,同样不报错但什么都不画,这个状态容易让人莫名其妙,如果画出来的图像里没有圆,可以先查一下半径值是不是被算成了负数。

4.2 rectangle函数:用“左上角+右下角”定义一个框

rectangle的常用签名有两种写法:

# 写法一:直接给两个对角顶点 cv2.rectangle(img, pt1, pt2, color, thickness=2) # 写法二:传一个Rect对象 cv2.rectangle(img, rect, color, thickness=2)

第一种写法里,pt1和pt2是矩形任意两个对角顶点,比如左上角(10, 10)和右下角(100, 100),画出来的是一个正常的矩形框。如果你把pt1和pt2传成同一行或同一列的两个点,画出来的矩形宽度或高度就是0,视觉上是条线,不报错。这个细节在做边界条件计算时非常容易踩雷——检测框坐标裁剪后可能出现某个维度为0的情况。

用rectangle函数画框是目标检测中最常见的可视化操作。实际项目里,你从深度学习模型拿到的一般是归一化的框坐标,比如center_x, center_y, width, height,需要先换算成左上角、右下角的像素坐标,再调用rectangle画图。我有一次就是忘了坐标转换,直接把归一化数值当成像素坐标传进去,结果画出来的检测框全部堆在图像左上角一片小区域里,排查了半天才定位到问题。

4.3 ellipse函数:角度、中心和长轴短轴的配合

ellipse的签名比较长:

cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness=1, lineType=cv2.LINE_8, shift=0)
  • center:椭圆中心
  • axes:长短轴半径(a, b),注意顺序是(长轴半径, 短轴半径),不是长短不要紧而是顺序别搞错
  • angle:整个椭圆绕中心的旋转角度,逆时针为正,单位是角度制不是弧度
  • startAngle和endAngle:绘制弧的起始角度和终止角度,0表示从右侧水平方向开始,逆时针扫过
  • thickness为-1时实心填充

ellipse最有意思的是这个startAngle和endAngle,它让ellipse能画完整的椭圆、椭圆弧甚至扇形。完全椭圆的条件就是startAngle=0、endAngle=360,或者startAngle=某个值、endAngle=这个值+360。如果你想要一个半圆弧,就填startAngle=0、endAngle=180,图形会从右侧的0度位置开始逆时针扫过上半部分。

我在项目里用ellipse画过圆形的仪表盘刻度;也在医疗图像处理Demo里用它把检测到的细胞核拟合成椭圆并画出来。在实际可视化中,如果你需要把目标区域用椭圆表示,可以先用cv2.fitEllipse函数求出一个旋转矩形包围盒,再把它画出来,fitEllipse返回的RotatedRect数据可以直接用ellipse函数可视化,这一点非常方便。

5. Rect类:理解它和rectangle函数的配合方式

5.1 Rect是数据封装,rectangle是画图操作

很多从Python入门OpenCV的朋友看到“Rect”会一头雾水:这到底是一个函数还是一个类?答案是:在C++里它是一个模板类——数据结构,用来表示一个矩形的几何信息;在Python接口里,OpenCV并没有单独暴露一个构造Rect的常用Python类,但我们在很多代码里会区分配置矩形的数据结构,比如(x, y, width, height)四元组,这实际上就是Rect类对应的数据语义。

理解Rect的关键在于:它是“矩形区域”这个概念,而不是“画矩形”的操作。rectangle函数负责把矩形画出来,Rect这种数据结构则负责承载矩形的位置和尺寸信息。在目标检测、图像裁剪、ROI提取这些任务中,你随时都需要描述“一块矩形区域”,这时候用(x, y, w, h)四元组来表达非常自然。比如要裁剪图像中间一块区域,你会写img[y:y+h, x:x+w],这里x、y、w、h就是Rect的含义。

5.2 Python中模拟Rect的常用操作

在Python里,我习惯用numpy或者简单的元组来表示Rect。OpenCV里有些函数会返回类似Rect结构的数据,比如boundingRect、selectROI等,返回值都是一个四元组。拿boundingRect举例:

import cv2 import numpy as np # 模拟一堆白色前景点的坐标 points = np.array([[10, 10], [50, 20], [30, 60], [40, 40]]) x, y, w, h = cv2.boundingRect(points) print(x, y, w, h) # 输出外接矩形的左上角坐标和宽高 # 通过这个外接矩形在原图上画出来 img = np.zeros((100, 100, 3), dtype=np.uint8) cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

这里bindRect返回的就是一个Rect语义的数据结构,拿到之后你还需要手动转换成两个对角顶点才能给rectangle函数用。很多刚接触的人会疑惑:rectangle能不能直接接收(x, y, w, h)这种格式?答案是不能,它要的是两个顶点坐标,所以必须自己做一次换算。这个转换也是一种经验的体现——我在写辅助工具时,通常封装一个小函数:rect2points(rect),输入四元组输出两个对顶点,这样代码里就清爽多了。

6. polylines画多边形:一个常常令人困惑的“点集类型”问题

6.1 参数格式与本质

polylines是画多边形和连续折线的函数,它和line、rectangle不一样的地方在于输入不再是两个点,而是一组点。常用签名:

cv2.polylines(img, pts, isClosed, color, thickness=1, lineType=cv2.LINE_8, shift=0)
  • pts:点集的列表,注意这里通常是list of arrays,数组维度有讲究
  • isClosed:布尔值,True表示画封闭多边形(首尾相连),False则画一条折线
  • color、thickness、lineType和前面一样

flatten这一层最容易出错的点集格式在Python里最坑:pts必须是N×1×2的numpy数组,或者列表外再套一层列表。很多新手直接传一个(N,2)的形状进去,结果报错:OpenCV Error: Assertion failed (npoints >= 0 && depth == CV_32S || depth == CV_32F) 。这个报错信息字面意思是“点数量非负,且数据类型必须是32位整数或浮点”,但实际上更多时候是pts的维度层级不对。

另一个非常典型的坑是数据类型。OpenCV要求polylines的点集必须是int32或float32类型。如果你用一个默认的int64 numpy数组传进去,也会报类型错误。解决方法是显式转换:

pts = np.array([[10, 10], [100, 30], [80, 90]], dtype=np.int32) pts = pts.reshape((-1, 1, 2)) # 调整为N×1×2的形状

6.2 完整的多边形绘制示例

import cv2 import numpy as np img = np.zeros((300, 300, 3), dtype=np.uint8) # 定义一个三角形的三个顶点,注意数据类型和维度 pts = np.array([[50, 50], [200, 50], [120, 200]], dtype=np.int32) pts = pts.reshape((-1, 1, 2)) # 画封闭三角形,线宽2 cv2.polylines(img, [pts], True, (0, 255, 255), 2) # 画一个不封闭的折线 pts2 = np.array([[30, 30], [100, 90], [150, 40], [260, 120]], dtype=np.int32) pts2 = pts2.reshape((-1, 1, 2)) cv2.polylines(img, [pts2], False, (255, 0, 0), 2)

我想特别强调一下为什么polylines要接受这种“列表套数组”的结构:因为pts是支持同时传入多个多边形/折线的,每个数组就是一条独立曲线,所以外层列表用来容纳多条曲线。如果你只画一条,也要按示例这样保持层级。

如果你只是想画个多边形并填充,polylines配合thickness=-1其实做不到,它的thickness传-1时行为与line一致——只填充线段本身,不是填充封闭区域。填充封闭多边形要改用cv2.fillPoly函数。很多初学者会混淆这两个函数,画出来发现中间是空的,以为是polylines的问题,其实换个函数就好了。

7. putText函数:图像上写字比你想象中多一点门道

7.1 参数与中文字体问题

putText的作用是在图像上绘制文字。它的签名:

cv2.putText(img, text, org, fontFace, fontScale, color, thickness=1, lineType=cv2.LINE_8, bottomLeftOrigin=False)
  • text:要绘制的字符串
  • org:文本左下角坐标(默认),注意是左下角不是左上角,这是文字排版里的baseline概念
  • fontFace:字体类型,OpenCV内置了几种,如cv2.FONT_HERSHEY_SIMPLEX、cv2.FONT_HERSHEY_DUPLEX、cv2.FONT_HERSHEY_COMPLEX
  • fontScale:字体缩放比例
  • bottomLeftOrigin:默认False表示图像坐标原点在左上角,文本从左上基准排版;如果设为True则文字底部坐标系统反转

putText最大的限制是:OpenCV自带的字体都不支持中文。你用putText直接写一个“你好”,画出来的大概率是一串问号。这在实际项目中很麻烦,比如做中文标注、界面提示、水印都会遇到这个问题。

解决中文的办法之一是使用第三方库PIL(Pillow)来绘制中文,然后再转换回OpenCV的BGR格式。这个思路核心就是“绕道”:先用PIL把文字渲染到透明图层上,然后将图层转成OpenCV图像,最后做图像叠加。很多开源项目都这么干。

PIL绘制中文的典型流程:

  1. 用ImageFont.truetype加载一个中文字体文件(比如系统中的微软雅黑或思源黑体)
  2. 创建一个透明或全黑的底图,用ImageDraw.Draw的text方法书写中文
  3. 转成numpy数组,把RGB转成BGR
  4. 和原图做加权叠加或者按掩模粘贴,完成中文标注

7.2 获取文字尺寸:getTextSize的用途

在实际标注中,我们经常需要把文字放在矩形框内,或者给文字画一个背景色块增强可读性。这时候需要先知道文字占用多大区域——getTextSize就是干这个的。

(text_width, text_height), baseline = cv2.getTextSize( "Hello", cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2 )

这个方法返回文字宽度、文字高度和基线偏移量。拿到尺寸后,就可以在文字左下角位置的上方画一个色块,再把文字叠上去,视觉上就像一个带背景的标签了。目标检测可视化时,我经常在检测框顶部画一个半透明的黑底,上面写类别和置信度,就是先getTextSize再填背景矩形,最后putText。

一个常用的封装长这样:

def draw_label(img, text, org, font_scale=1.0, thickness=2, bg_color=(0,0,0), text_color=(255,255,255)): # 获取文字尺寸 (w, h), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness) x, y = org # 绘制背景矩形,给一点 padding cv2.rectangle(img, (x, y - h - baseline - 5), (x + w + 10, y + baseline + 5), bg_color, -1) cv2.putText(img, text, (x + 5, y), cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, thickness) return img

注意putText的org是文字左下角的基线位置,所以在背景矩形里我用了(y - h - baseline)作为顶部边界,这样才能包住完整字符区域。这个细节不做一次实际标注很难注意到。

8. 综合实战:一个可运行的图像标注Demo

前面把每个函数拆开讲了,这一节我做一个综合示例,把所有函数串起来,模拟一个目标检测结果的标注场景。你运行这段代码,会得到一张包含矩形框、实心圆、椭圆、文字标签、多边形区域的图像。

import cv2 import numpy as np def draw_detection_result(): # 模拟一张640x480的输入图像,这里用随机噪声代替真实画面 img = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) # 在图像中央画一个矩形检测框 box_x, box_y, box_w, box_h = 150, 120, 320, 220 cv2.rectangle(img, (box_x, box_y), (box_x + box_w, box_y + box_h), (0, 255, 0), 3) # 在检测框中心画一个实心圆,模拟关键点 center_x = box_x + box_w // 2 center_y = box_y + box_h // 2 cv2.circle(img, (center_x, center_y), 8, (0, 0, 255), -1) # 在检测框左上角画一个标签背景 + 文字 label_text = "object: 0.87" (tw, th), baseline = cv2.getTextSize(label_text, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2) label_x, label_y = box_x, box_y - 5 cv2.rectangle(img, (label_x, label_y - th - baseline - 5), (label_x + tw + 10, label_y + baseline + 5), (0, 255, 0), -1) cv2.putText(img, label_text, (label_x + 5, label_y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) # 用椭圆圈出感兴趣区域 cv2.ellipse(img, (320, 240), (150, 80), 0, 0, 360, (255, 0, 0), 2) # 用多边形画一个感兴趣区域(不规则ROI) roi_pts = np.array([[90, 360], [210, 390], [350, 360], [420, 420], [200, 440]], dtype=np.int32) roi_pts = roi_pts.reshape((-1, 1, 2)) cv2.polylines(img, [roi_pts], True, (0, 165, 255), 2) # 用一条线段连接关键点和ROI中心,模拟路径 cv2.line(img, (center_x, center_y), (320, 240), (255, 255, 255), 2) # 在图像左上角实时显示FPS cv2.putText(img, "FPS: 30", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) return img if __name__ == '__main__': result = draw_detection_result() cv2.imshow('Detection Visualization', result) cv2.waitKey(0) cv2.destroyAllWindows()

这段代码基本上把本文所有函数都用上了。你可以试着调换参数,比如把ellipse的angle改成45度、startAngle改成0、endAngle改成180,效果完全不同。这种动手改参数的方式,比看十遍文档都记得牢。

综合实践的思路也同样可以迁移到别的领域:骨架关键点可视化、答题卡识别时的选项框标注、车牌字符区域框选等,核心都是“先计算坐标,再调用对应的画图函数”。

9. 典型问题排查:画图相关报错和效果异常处理速查表

我在做项目时积累了一些画图相关的报错和效果异常问题,整理成速查表,你可以直接当参考。

现象大概率原因解决方案
运行后图像没有任何图形color被写成(0,0,0)黑色,或thickness=0检查颜色值、thickness≥1
图形“消失”但程序正常运行坐标严重越界被裁剪在画图前打印坐标,核对图像宽高
color写(255,0,0)但画出来是蓝色误用了RGB顺序改用(0,0,255)表示红色
polylines报错depth == CV_32S点集dtype不是int32显式np.int32转换
polylines仍报维度错误pts维度不对reshape(-1,1,2)
putText写中文出现问号OpenCV原生字体库无中文用PIL绘制中文再转BGR
在视频流里画图很卡每帧都新建画布或频繁分配大数组复用画布,只在需要时更新ROI
rectangle画出来是线条而非矩形pt1和pt2在同一水平线或垂直线上检查坐标运算,打印调试
实心圆没被填充thickness忘记传-1使用thickness=-1
椭圆画出来旋转角度不对角度传成弧度确认是角度制,必要时用math.degrees转换
画出的图像颜色整体发灰uint8浮点类型错误或画布类型不对确保numpy数组dtype为uint8
运行后出现“Assertion failed”某个参数类型不符合C++底层断言查看报错行号对应参数,逐一核对

这里面有几个是我反复踩过的坑。比如polylines的dtype问题,我一开始总是不理解为什么OpenCV的Python接口这么“事儿多”,非要用int32,后来想明白了,因为底层C++实现里它要求32位有符号整型的点坐标,传int64给绑定层时无法自动降级,所以只能自己转换。这类问题如果不知道背后原理,光看报错信息真的很难定位。

再比如putText的中文问题。我在某图像检测项目中做全中文的界面临时调试输出,以为直接用putText就能搞定,结果在画面上看到一串“???”,排查了很久才发现不是代码逻辑错了,而是OpenCV原生字体根本不支持中文。后来用PIL方案解决了,但每次写中文标注都要多走一步图像转换的流程,这也算是一个不大不小的坑。

10. 我的个人经验与扩展建议

画图函数看着简单,实际用起来门道不少。这里我说几个自己写代码时候总结的习惯,希望能给你一些参考。

第一,尽量把画图操作封装成独立函数。不要在主逻辑里到处撒cv2.rectangle、cv2.putText,而是封装成类似draw_bbox、draw_label、draw_point这样的工具函数。这样代码清晰,调试方便,画图风格也统一。我在标注工具、检测可视化脚本里都是这么做的,后期维护起来轻松太多。

第二,在多线程或实时视频处理场景里使用画图函数时,记得注意画布共享的问题。OpenCV的绘图函数是原地修改图像的,如果多个线程同时往同一张图上画,可能出现意料之外的覆盖。我的习惯是做好图像拷贝,或者用锁保护关键绘图逻辑。

第三,做界面展示时,颜色方案最好提前定义成常量。比如定义GREEN=(0,255,0)、RED=(0,0,255)、BLUE=(255,0,0),这样代码里到处都是魔法数字的情况会减少很多,也方便以后统一换主题色。

如果你对OpenCV画图这块还想继续深挖,我建议往这几个方向拓展:fillPoly和fillConvexPoly的填充逻辑、drawContours在轮廓可视化中的用法、arrowedLine做方向标注、putText的PIL中文方案细节。这几个方向在实际项目和比赛中都经常用到,而且和本文的函数几乎可以无缝衔接。

最后再分享一个小技巧:如果你在调试时嫌opencv的imshow窗口太小,可以用命名窗口的WINDOW_NORMAL属性自由缩放窗口大小。配合画图函数做可视化时,这个设置非常实用。

cv2.namedWindow('img', cv2.WINDOW_NORMAL) cv2.resizeWindow('img', 960, 720) cv2.imshow('img', result)

从这个角度来说,画图函数也许不“高大上”,但它是整个OpenCV使用频率最高、确认感最强的基础能力。只要你把这些函数的底层逻辑和细节摸透了,后面做任何视觉任务的可视化都会顺手很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进