OpenCV图像宽高获取与矩形框标注:从坐标越界到精准绘制

发布时间:2026/10/21 8:42:41
OpenCV图像宽高获取与矩形框标注:从坐标越界到精准绘制 1. 从一次坐标越界说起为什么宽高和矩形框总是一起出现刚接触图像处理那会儿我写过一段自认为没问题的代码读一张图画个矩形框把目标圈出来然后保存。结果跑起来直接报错提示坐标超出了图像范围。当时我盯着屏幕愣了半天——明明框的位置是我用肉眼看图估出来的怎么会越界后来才反应过来我压根没确认过这张图的实际宽高是多少纯粹凭感觉填了坐标。这件事让我意识到一个很基础但极容易被忽略的事实在图像处理里宽高是所有坐标操作的基准。你画矩形框、裁剪区域、缩放、做ROI提取全都依赖这两个数字。不知道宽高就动手等于蒙着眼睛走路。这篇内容就围绕这个看似简单、实则暗藏不少细节的主题展开。核心讲三件事怎么用OpenCV准确拿到图片的宽度和高度、拿到之后怎么用、以及矩形框标注这件事从坐标理解到实际绘制有哪些坑。适合刚入门图像处理的朋友也适合写过一些代码但总在坐标上翻车的人。全程用Python和OpenCV演示代码可以直接复制去跑。需要先说明一点宽高看起来就是两个整数但它们的获取方式、顺序、以及和后续标注操作的配合里面有不少值得说清楚的地方。很多人第一次用OpenCV时会把宽和高的顺序搞反这个错误在画框的时候会直接体现出来而且报错信息往往不够直观排查起来挺费劲。2. 图片宽高的本质shape返回的到底是个什么结构2.1 一张图在计算机里是怎么存的要理解宽高得先知道一张彩色图片在内存里长什么样。你可以把图片想象成一个三层叠放的网格本最上面一层记录每个像素的红色分量中间一层记录绿色下面一层记录蓝色。每一层都是一个二维表格表格的行数对应图像的高度列数对应图像的宽度。用OpenCV读进来的图片本质上就是一个三维的NumPy数组形状是(高度, 宽度, 通道数)。注意这里的顺序——高度在前宽度在后。这是很多人第一次接触时会栽跟头的地方因为日常说话都是宽高但数组里偏偏是高宽。import cv2 img cv2.imread(test.jpg) print(img.shape) # 输出类似(480, 640, 3)上面这个输出意味着这张图高480像素宽640像素有3个颜色通道BGR。如果你把480当成宽度去用后面画框、裁剪全会错位。2.2 为什么OpenCV用高在前、宽在后这不是OpenCV故意为难人而是和它的底层数据结构有关。图像在内存中按行存储一行一行往下排。访问的时候先定位到第几行高度方向再定位到第几列宽度方向所以shape自然就是(行数, 列数, 通道)。行数就是高度列数就是宽度。这个逻辑和数学里的矩阵是一致的一个m行n列的矩阵shape是(m, n)。图像就是特殊的矩阵只不过多了通道这个维度。提示如果你之前用过PILPillow会发现它的size属性返回的是(宽度, 高度)和OpenCV正好相反。混用两个库的时候特别容易搞错建议在代码里统一用OpenCV的shape方式或者做好注释。2.3 灰度图和带透明通道的图shape有什么不同不是所有图片都是三个通道。灰度图只有一层shape是二维的gray cv2.imread(test.jpg, cv2.IMREAD_GRAYSCALE) print(gray.shape) # 输出类似(480, 640)这时候没有第三个维度gray.shape[0]是高度gray.shape[1]是宽度。还有一种带透明通道的PNG图用cv2.IMREAD_UNCHANGED读取时会有4个通道BGRAimg_alpha cv2.imread(test.png, cv2.IMREAD_UNCHANGED) print(img_alpha.shape) # 输出类似(480, 640, 4)通道数变了但前两个维度仍然是高度和宽度这一点不变。所以取宽高的时候永远认准shape[0]是高度、shape[1]是宽度不管通道数是多少。2.4 取宽高的几种写法与常见错误最直接的写法height, width img.shape[:2] print(f宽度: {width}, 高度: {height})用shape[:2]切片取前两个维度然后按顺序解包给height和width。这种写法清晰推荐使用。另一种写法是分别取height img.shape[0] width img.shape[1]效果一样但稍微啰嗦一点。常见的错误有这么几种。第一种是顺序搞反# 错误示范 width, height img.shape[:2] # 这样width拿到的是高度第二种是忘了灰度图没有第三维写了img.shape[2]去取通道数结果灰度图直接报索引越界。稳妥的做法是先判断维度if len(img.shape) 2: height, width img.shape channels 1 else: height, width, channels img.shape第三种是图片读取失败返回None然后去取shape直接崩掉。这个坑很常见路径写错、文件损坏、格式不支持都会导致imread返回None。所以读图之后最好加一个判断img cv2.imread(test.jpg) if img is None: print(图片读取失败检查路径和文件) else: height, width img.shape[:2]这个习惯能帮你省下不少排查时间。我见过太多人对着AttributeError: NoneType object has no attribute shape发呆其实问题根本不在shape而在图片压根没读进来。3. 矩形框标注的坐标逻辑从左上角到右下角3.1 OpenCV的坐标系和你想的可能不一样数学课上画的坐标系原点在左下角y轴往上增长。但图像处理里的坐标系是反过来的原点在左上角x轴向右增长y轴向下增长。这意味着一个点的坐标(x, y)x越大越靠右y越大越靠下。图片的左上角是(0, 0)右下角是(width-1, height-1)。注意是width-1和height-1因为像素索引从0开始。这个坐标系方向的问题直接影响到矩形框的绘制。如果你按数学课的习惯去理解画出来的框位置会上下颠倒。3.2 矩形框的两个关键点OpenCV画矩形用的是cv2.rectangle函数核心参数是两个点左上角和右下角。cv2.rectangle(img, pt1, pt2, color, thickness)pt1矩形左上角坐标格式(x, y)pt2矩形右下角坐标格式(x, y)color颜色BGR格式的元组比如(0, 255, 0)是绿色thickness线宽整数-1表示填充假设我要在图片中间画一个框图片宽640高480框的左上角在(200, 150)右下角在(440, 330)cv2.rectangle(img, (200, 150), (440, 330), (0, 255, 0), 2)这里框的宽度是440-200240像素高度是330-150180像素。两个点的x差值就是框宽y差值就是框高。3.3 为什么坐标越界不会总是报错这里有个很坑的地方OpenCV的绘图函数对越界坐标往往不会报错而是默默地画到图像外面去你在结果图里什么都看不到或者只看到框的一部分。比如图片是640x480你写了个右下角坐标(800, 600)程序照跑不误但框的右边和下边超出了图像范围显示出来就是残缺的。这种不报错的错误最难排查因为你以为代码没问题但结果就是不对。所以画框之前养成一个习惯确认坐标在合法范围内。x1, y1, x2, y2 200, 150, 440, 330 x1 max(0, min(x1, width - 1)) y1 max(0, min(y1, height - 1)) x2 max(0, min(x2, width - 1)) y2 max(0, min(y2, height - 1))这段钳制代码把坐标限制在图像范围内虽然简单但能避免很多莫名其妙的显示问题。3.4 从检测结果到画框坐标格式的转换实际项目中矩形框的坐标往往来自目标检测模型或者标注文件。不同来源的坐标格式不一样常见的有两种格式表示方式说明两点式(x1, y1, x2, y2)左上角和右下角中心宽高式(cx, cy, w, h)中心点坐标加宽高OpenCV画框需要两点式。如果你拿到的是中心宽高式得先转换cx, cy, w, h 320, 240, 240, 180 x1 int(cx - w / 2) y1 int(cy - h / 2) x2 int(cx w / 2) y2 int(cy h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)转换的时候注意取整因为像素坐标必须是整数。用int()直接截断还是用round()四舍五入影响不大但整个项目里最好统一。还有一种情况是归一化坐标值在0到1之间需要乘以宽高还原成像素坐标# 假设归一化坐标是 (nx1, ny1, nx2, ny2) x1 int(nx1 * width) y1 int(ny1 * height) x2 int(nx2 * width) y2 int(ny2 * height)这里就用到了前面获取的width和height。归一化坐标的好处是和图像尺寸无关换一张不同分辨率的图坐标含义不变但画框时必须还原。4. 把宽高和画框串起来一个完整的实操流程4.1 完整代码骨架下面这段代码把读图、取宽高、画框、保存串成一条线可以直接拿去改import cv2 def draw_box(image_path, box, output_pathoutput.jpg): # 读取图片 img cv2.imread(image_path) if img is None: print(图片读取失败) return # 获取宽高 height, width img.shape[:2] print(f图像尺寸: 宽{width} x 高{height}) # 解析框坐标 x1, y1, x2, y2 box # 坐标钳制 x1 max(0, min(x1, width - 1)) y1 max(0, min(y1, height - 1)) x2 max(0, min(x2, width - 1)) y2 max(0, min(y2, height - 1)) # 画框 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 保存 cv2.imwrite(output_path, img) print(f已保存到 {output_path}) draw_box(test.jpg, (200, 150, 440, 330))这段代码里宽高的作用体现在两个地方一是打印出来让你心里有数二是坐标钳制时作为边界。如果框坐标是归一化的宽高还会参与坐标还原。4.2 加文字标签时宽高又派上用场画完框通常还要加个类别标签。文字的位置一般放在框的左上角上方但如果框太靠上文字会超出图像顶部。这时候可以用高度做个判断label target font cv2.FONT_HERSHEY_SIMPLEX font_scale 0.6 thickness 2 # 计算文字尺寸 (text_w, text_h), baseline cv2.getTextSize(label, font, font_scale, thickness) # 文字默认放在框上方如果上方空间不够就放到框内部 text_y y1 - 10 if y1 - text_h - 10 0 else y1 text_h 10 cv2.putText(img, label, (x1, text_y), font, font_scale, (0, 255, 0), thickness)这里用到了y1和图像高度的关系来判断文字该放上面还是下面。如果不做这个判断文字被截断是常有的事。4.3 批量处理时宽高不一致怎么办实际项目里经常要处理一批图片每张图的宽高可能都不一样。这时候有几种策略第一种是统一缩放到固定尺寸比如都缩到640x640。这样后续坐标处理简单但会改变目标的宽高比可能导致变形。target_size (640, 640) img_resized cv2.resize(img, target_size)注意cv2.resize的尺寸参数是(宽度, 高度)又是宽在前。这里和shape的顺序相反特别容易搞混。我的建议是写代码时把变量名写清楚比如target_width, target_height 640, 640然后cv2.resize(img, (target_width, target_height))减少出错概率。第二种是保持原尺寸每张图单独取宽高坐标按各自的比例处理。这种方式不损失信息但代码里要处处传递宽高稍微麻烦一点。第三种是letterbox缩放保持宽高比的同时缩放到目标尺寸空白处填充。这种方式在目标检测里很常见因为不变形。但坐标还原的时候要减去填充偏移再除以缩放比例计算量稍大。选哪种取决于你的下游任务。如果只是显示和标注第二种最省事。如果要送进固定输入的模型第一种或第三种更合适。4.4 保存结果时的格式陷阱cv2.imwrite保存图片时格式由文件扩展名决定。写.jpg就是JPEG写.png就是PNG。但有个细节JPEG是有损压缩反复保存会累积画质损失。如果你要多次读写中间结果建议用PNG。另外如果图片路径包含中文在某些环境下imread和imwrite会失败。稳妥的做法是用cv2.imencode配合文件写入# 处理中文路径的保存方式 ext .jpg success, encoded cv2.imencode(ext, img) if success: with open(输出图片.jpg, wb) as f: f.write(encoded.tobytes())读取中文路径也类似用np.fromfile加cv2.imdecodeimport numpy as np img cv2.imdecode(np.fromfile(中文路径.jpg, dtypenp.uint8), cv2.IMREAD_COLOR)这个坑在国内环境里挺常见的提前知道能省不少事。5. 那些年我在宽高和画框上踩过的坑5.1 宽高顺序搞反导致的隐形错误前面提过shape是高在前宽在后但真正坑人的是当你把宽高搞反后代码不一定报错。比如你要在图片中心画个点center_x width // 2 center_y height // 2如果width和height互换了画出来的点位置就偏了。图片如果不是正方形这个偏移肉眼可见但如果你不知道原图尺寸可能以为是正常的。更隐蔽的是裁剪操作# 想裁剪出中间区域 crop img[height//4 : height*3//4, width//4 : width*3//4]如果宽高反了裁剪出来的区域形状完全不对但代码照样跑。这种错误只能靠对图像尺寸的敏感度来发现。我的经验是在代码里永远用height, width img.shape[:2]这种明确解包的方式不要用h, w img.shape[:2]之外的简写更不要用img.shape[0]和img.shape[1]混着用而不加注释。变量名写全虽然多敲几个字符但省下的调试时间远超这点成本。5.2 画框线宽为负数会怎样cv2.rectangle的thickness参数如果传-1表示填充整个矩形。这个特性有时候有用比如做掩膜或者高亮区域。但如果你本来想画个边框手误传了-1整个框就变成实心色块把目标完全遮住了。cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), -1) # 填充 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 边框还有一种情况是thickness传了0这时候OpenCV的行为是不画任何东西也不报错。所以线宽至少给1。5.3 颜色顺序是BGR不是RGBOpenCV读图和绘图都用BGR顺序。你想画红色框写(255, 0, 0)不是(0, 0, 255)。这个坑几乎每个初学者都会踩一次。# 红色框BGR cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) # 绿色框 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 蓝色框 cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)如果你用matplotlib显示OpenCV读进来的图颜色会反过来因为matplotlib用RGB。这时候需要转换img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB)5.4 图片读取失败但没检查这个坑前面提过但值得再强调一次。cv2.imread在文件不存在、路径错误、格式不支持、权限不足等情况下都返回None而且不抛异常。如果你直接img.shape就会得到AttributeError: NoneType object has no attribute shape。这个报错信息其实已经提示了问题所在——img是None。但很多人第一反应是去查shape的用法方向就偏了。正确的做法是读图之后立刻判断img cv2.imread(path) assert img is not None, f无法读取图片: {path}用assert在开发阶段快速暴露问题比后面莫名其妙报错要好排查得多。5.5 坐标取整方式不一致导致的框偏移当坐标来自浮点计算时取整方式会影响框的位置。比如int(200.9)得到200round(200.9)得到201。如果一部分坐标用int一部分用round框可能会偏移一两个像素。一两个像素在大多数场景下无所谓但在高精度标注或者小目标检测里可能就有影响。建议整个项目统一取整方式我一般用int(round(x))先四舍五入再转整数兼顾精度和一致性。6. 几个提升效率的实用技巧6.1 封装一个通用的画框函数与其每次写一堆重复代码不如封装一个函数把宽高获取、坐标钳制、画框、加标签都包进去def draw_boxes(image_path, boxes, labelsNone, color(0, 255, 0), thickness2): img cv2.imread(image_path) if img is None: raise ValueError(f无法读取图片: {image_path}) height, width img.shape[:2] for i, box in enumerate(boxes): x1, y1, x2, y2 map(int, box) x1 max(0, min(x1, width - 1)) y1 max(0, min(y1, height - 1)) x2 max(0, min(x2, width - 1)) y2 max(0, min(y2, height - 1)) cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness) if labels and i len(labels): label labels[i] (tw, th), _ cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) ty y1 - 8 if y1 - th - 8 0 else y1 th 8 cv2.putText(img, label, (x1, ty), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img这个函数处理了坐标钳制、标签位置自适应调用的时候只需要传图片路径和框列表。6.2 用宽高做简单的越界预警在开发阶段可以加一段检查逻辑当框坐标超出图像范围时打印警告def check_box(box, width, height): x1, y1, x2, y2 box if x1 0 or y1 0 or x2 width or y2 height: print(f警告: 框{box}超出图像范围({width}x{height})) return False return True这样在批量处理时能快速定位有问题的数据而不是等到看图才发现框画歪了。6.3 宽高信息在日志里的价值处理大批量图片时把每张图的宽高记录到日志里后面排查问题会方便很多。比如你发现某张图的框位置不对翻日志一看这张图是320x240而其他图都是1920x1080那很可能是坐标没有按比例适配。import logging logging.basicConfig(levellogging.INFO) height, width img.shape[:2] logging.info(f处理图片: {image_path}, 尺寸: {width}x{height})日志看起来不起眼但在出问题的时候它是你回溯现场的唯一线索。6.4 关于宽高和画框还有这些可以延伸掌握了基础的宽高获取和矩形框绘制之后可以往几个方向继续深入。一个是旋转矩形框OpenCV提供了cv2.boxPoints和cv2.drawContours来画带角度的框适合处理倾斜目标。另一个是多边形标注用cv2.polylines画任意形状比矩形更灵活。还有一个方向是交互式标注用cv2.setMouseCallback监听鼠标事件让用户手动拖拽画框。这个在做标注工具的时候很有用核心逻辑就是记录鼠标按下和抬起的坐标然后画框。不管往哪个方向走宽高这两个基础信息始终是绕不开的。把它们的获取方式、顺序、和坐标的关系搞清楚后面的事情都会顺很多。我在带新人的时候通常会让他们先把这几个基础操作反复练几遍练到不用查文档就能写出来再往下进行。基础打牢了后面遇到再复杂的场景心里都有底。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询