资讯动态

OpenCV图像宽高获取与矩形框标注实战指南

发布时间:2026/10/22 14:32:28 来源:尧图企业网站定制
1. 从一张图说起为什么宽高和矩形框标注是视觉项目的“地基”做视觉项目的人都有一个共识不管上层跑的是多复杂的模型落到工程实现里第一步永远是搞清楚“这张图到底多大”“我要处理的那块区域在哪”。cv2.imread读进来一个numpy.ndarray它的shape属性就三个值——高度、宽度、通道数。听起来简单到不值一提但我见过太多项目在这上面翻车坐标越界、框画歪、缩放后标注错位、批量处理时把宽高写反导致整批数据报废。这篇文章聊的就是这个“地基级”的操作用 OpenCV 获取图片的宽度和高度以及在此基础上做矩形框标注。它看起来是入门内容但真正把它用扎实涉及到坐标系约定、图像属性读取的多种方式、矩形绘制的参数细节、批量处理的健壮性设计以及标注结果如何落地成可复用的数据。适合刚接触 OpenCV 的开发者打基础也适合有一定经验但想把自己代码写得更稳的人对照检查。我个人的习惯是任何一个视觉项目先写一个“图像体检”函数把尺寸、通道、数据类型打印出来再动手做后面的事。这个习惯帮我省下了无数次调试时间。下面就把这套东西完整拆开讲。2. 图像尺寸获取三种写法与它们背后的取舍2.1 shape 属性最直接也最容易被坑的方式OpenCV 用cv2.imread读图返回的是一个多维数组。对彩色图来说它的结构是(高度, 宽度, 通道数)。注意这个顺序——高度在前宽度在后。这是新手最容易搞反的地方因为日常说话都是“宽高”但数组里偏偏是“高宽”。import cv2 img cv2.imread(sample.jpg) print(img.shape) # 例如 (720, 1280, 3) height, width img.shape[:2] print(fwidth{width}, height{height})为什么 OpenCV 要用这个顺序因为图像在内存里是按行存储的一行一行往下排行数就是高度每行的像素数就是宽度。numpy的索引习惯是img[row, col]也就是img[y, x]。理解了这一点后面画矩形框时坐标顺序就不会乱。注意如果图片读取失败img会是None此时访问.shape会直接抛AttributeError。生产代码里必须先判空。2.2 用img.shape还是img.shape[:2]一个实际影响很多人写代码时习惯h, w, c img.shape这在彩色图上没问题但一旦遇到灰度图cv2.imread(path, cv2.IMREAD_GRAYSCALE)shape 只有两个值解包就会报“not enough values to unpack”。所以更稳的写法是h, w img.shape[:2]这样无论彩色还是灰度都能正常工作。通道数需要时再单独取img.shape[2]并做判断。这个细节在写通用工具函数时特别重要因为你永远不知道调用方传进来的是几通道的图。2.3 尺寸获取的另外两条路ndarray.size与图像元信息除了 shape还有两个信息值得知道img.size返回的是像素总数等于h * w * c。它不能直接告诉你宽高但可以用来做快速校验比如判断两张图是否像素总量一致。如果只是想拿尺寸而不想真正解码整张图大图场景下很实用可以用cv2.imread之外的方式读取文件头信息。不过在纯 OpenCV 体系里最常用的还是 shape。我实测过一个场景批量处理几千张高分辨率图时如果只是为了统计尺寸分布先全部imread再取 shape 会非常慢。这时候可以先读文件头拿尺寸做筛选再决定要不要真正解码。这个优化在数据清洗阶段能省下大量时间。2.4 尺寸相关的常见坑与规避问题现象根本原因解决方式宽高写反框画到图外shape 顺序是高在前统一用h, w img.shape[:2]并加注释灰度图解包报错通道数不存在用切片[:2]而非全解包读图返回 None 后崩溃路径错误或格式不支持读后立即判空并给出明确日志缩放后坐标错位标注坐标基于原图缩放比例同步作用于坐标这张表里的每一条我都在真实项目里踩过至少一次。尤其是最后一条做图像增强或统一尺寸时如果只缩放图像不缩放标注框框就会整体偏移而且偏移量随缩放比例变化非常隐蔽。3. 矩形框标注从坐标约定到绘制细节3.1 坐标系约定左上角是原点OpenCV 的图像坐标系原点在左上角x 轴向右y 轴向下。一个矩形框用两个点表示左上角(x, y)和右下角(x w, y h)。cv2.rectangle接受的就是这两个点。x, y, w, h 100, 80, 200, 150 cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2)这里有个容易混淆的点很多标注工具比如某些数据集格式用的是(x_min, y_min, x_max, y_max)而另一些用的是(x, y, w, h)。这两种表示法在转换时极易出错。我的做法是在项目里统一用一种内部格式所有外部输入输出都写明确的转换函数绝不混用。3.2cv2.rectangle的参数逐个拆解cv2.rectangle(img, pt1, pt2, color, thickness, lineType, shift)img目标图像函数是原地修改会直接改变传入的数组。pt1、pt2矩形的两个对角点顺序无所谓函数会自动处理。colorBGR 顺序的元组。注意是 BGR 不是 RGB(0, 255, 0)是绿色(255, 0, 0)是蓝色。这个坑几乎每个新手都踩过。thickness线宽。设为-1或cv2.FILLED会填充整个矩形。lineType线条类型默认cv2.LINE_8。画细线时用cv2.LINE_AA抗锯齿视觉上更平滑。shift坐标的小数位数用于亚像素精度。一般用不到但做精细标注时有用。提示cv2.rectangle会修改原图。如果你需要保留原图先img.copy()一份再画。3.3 颜色与线宽的实战选择颜色选择不只是好看的问题。在标注场景里颜色承担着“区分类别”的功能。比如检测任务里人用绿色、车用蓝色、交通标志用红色。我一般会准备一个颜色表按类别索引取色避免每次手写 BGR 值。线宽的选择和图像分辨率强相关。1080p 的图用 2 像素线宽刚好4K 图就得用 4 到 6 像素否则框细得几乎看不见。一个经验公式是thickness max(2, int(min(h, w) / 400))让线宽随图像尺寸自适应。3.4 在框上叠加文字标签光有框还不够通常还要在框旁边写上类别和置信度。cv2.putText可以做到但要注意文字位置别超出图像边界。label person 0.92 (tw, th), baseline cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) cv2.rectangle(img, (x, y - th - baseline), (x tw, y), (0, 255, 0), -1) cv2.putText(img, label, (x, y - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)这段代码先画一个填充的小矩形作为文字背景再在上面写黑字。这样无论背景是什么颜色文字都清晰可读。getTextSize返回文字的宽高和基线偏移用它来动态计算背景框大小比写死数值靠谱得多。4. 完整实操一个可复用的标注工具函数4.1 需求拆解与函数设计把前面的知识点串起来做一个实用的小工具输入一张图和一个框列表输出画好框的图同时打印图像尺寸信息。框列表里每个元素包含坐标、类别名和可选置信度。设计上要考虑几点图像读取失败要能优雅退出框坐标要做边界裁剪防止画到图外颜色按类别自动分配文字背景自适应。这些都是在实际使用中逐渐补上的一开始写得很简单用着用着问题就冒出来了。4.2 核心代码实现import cv2 import numpy as np def get_image_size(image_path): img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图像: {image_path}) h, w img.shape[:2] c img.shape[2] if img.ndim 3 else 1 return w, h, c def draw_boxes(image_path, boxes, output_pathNone): img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图像: {image_path}) h, w img.shape[:2] thickness max(2, int(min(h, w) / 400)) color_map {} for box in boxes: x, y, bw, bh box[x], box[y], box[w], box[h] label box.get(label, obj) score box.get(score) # 边界裁剪 x1 max(0, min(x, w - 1)) y1 max(0, min(y, h - 1)) x2 max(0, min(x bw, w - 1)) y2 max(0, min(y bh, h - 1)) if label not in color_map: idx len(color_map) color_map[label] ( int((37 * idx 50) % 256), int((17 * idx 120) % 256), int((29 * idx 200) % 256), ) color color_map[label] cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness) text f{label} {score:.2f} if score is not None else label font_scale max(0.5, min(h, w) / 1200) (tw, th), baseline cv2.getTextSize( text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness ) ty y1 - th - baseline if ty 0: ty y2 cv2.rectangle(img, (x1, ty), (x1 tw, ty th baseline), color, -1) cv2.putText( img, text, (x1, ty th), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), thickness ) if output_path: cv2.imwrite(output_path, img) return img4.3 参数计算过程说明线宽和字体大小都用了自适应公式这里解释一下思路。min(h, w) / 400的含义是图像短边每 400 像素对应 1 像素线宽。1080p 图短边 1080算出来约 2.7取整后 24K 图短边 2160算出来约 5.4取整后 5。这样不同分辨率下视觉粗细基本一致。字体缩放用min(h, w) / 1200同样是让文字大小随图像尺寸走。这两个系数不是理论推导出来的是我在多种分辨率下反复调整后觉得比较舒服的值你可以根据自己的审美微调。颜色生成用了三个不同的线性同余公式目的是让相邻类别的颜色差异明显。(37 * idx 50) % 256这类写法能保证在 idx 递增时颜色值跳变较大避免出现两个类别颜色接近的情况。这比随机取色更可控因为随机取色可能连续两次取到相近的颜色。4.4 调用示例与输出验证boxes [ {x: 120, y: 90, w: 200, h: 260, label: person, score: 0.93}, {x: 400, y: 150, w: 180, h: 180, label: car, score: 0.87}, ] draw_boxes(input.jpg, boxes, output.jpg)跑完之后打开output.jpg检查三件事框的位置是否和目标准确对齐、文字是否清晰可读、颜色是否容易区分。如果框整体偏移多半是坐标格式搞混了如果文字被截断检查ty的计算和边界处理。5. 批量处理与数据落地让标注结果真正可用5.1 批量读取目录并统计尺寸分布单个图处理只是开始实际项目里往往是成百上千张。批量处理时第一件事是统计尺寸分布看看有没有异常尺寸的图。import os from collections import Counter def scan_sizes(folder): counter Counter() for name in os.listdir(folder): if not name.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue path os.path.join(folder, name) img cv2.imread(path) if img is None: print(f[跳过] 无法读取: {name}) continue h, w img.shape[:2] counter[(w, h)] 1 return counter这个函数返回一个尺寸计数字典。如果发现某张图尺寸特别离谱比如 1x1 或者几万像素宽基本可以判定是损坏文件或异常数据需要单独处理。我在一个项目里就遇到过一张 0 字节的图imread返回 None如果不判空直接取 shape 就会中断整个批处理流程。5.2 标注结果的存储格式选择画在图上只是给人看的真正要喂给下游流程的是结构化的标注数据。常见格式有几种格式坐标表示特点内部字典x, y, w, h灵活适合程序内部流转通用交换格式x_min, y_min, x_max, y_max跨工具兼容性好归一化格式相对宽高的比例值与分辨率解耦适合训练归一化格式特别值得说。它把坐标除以图像宽高变成 0 到 1 之间的小数。这样无论原图多大标注都能直接对应。训练模型时几乎都用这种格式因为输入尺寸经常要统一缩放归一化坐标不受缩放影响。def to_normalized(box, img_w, img_h): return { cx: (box[x] box[w] / 2) / img_w, cy: (box[y] box[h] / 2) / img_h, nw: box[w] / img_w, nh: box[h] / img_h, }这里用的是中心点加宽高的表示法也是很多检测框架的标准输入格式。转换时务必用浮点除法Python 3 里/本身就是浮点除法但如果你从其他语言转过来要留意整数除法的陷阱。5.3 批量标注的健壮性设计批量跑的时候最怕的是中途崩溃导致前面全白干。我的做法是每处理完一张就写一次结果用追加模式写 JSON Lines 文件一行一条记录。这样即使中途出错已完成的部分也是安全的。import json def batch_annotate(folder, boxes_map, out_jsonl): with open(out_jsonl, w, encodingutf-8) as f: for name, boxes in boxes_map.items(): path os.path.join(folder, name) img cv2.imread(path) if img is None: f.write(json.dumps({file: name, error: read_failed}) \n) continue h, w img.shape[:2] record { file: name, width: w, height: h, boxes: [to_normalized(b, w, h) for b in boxes], } f.write(json.dumps(record, ensure_asciiFalse) \n)JSON Lines 的好处是流式可读不用一次性加载整个文件出错了也能快速定位到具体哪一行。相比一个大 JSON 数组它在处理大量数据时更稳。6. 常见问题排查与避坑经验6.1 坐标越界与负值处理标注数据来自人工或模型预测时出现负坐标或超出图像范围的坐标是家常便饭。如果不做裁剪直接画OpenCV 不会报错但框会画到看不见的地方或者只画出一部分让人误以为标注丢了。我的处理原则是先裁剪再绘制同时记录裁剪前的原始值。因为裁剪会改变框的实际范围如果下游需要精确的原始坐标得保留一份。裁剪逻辑就是前面代码里的max(0, min(x, w-1))那一套简单但有效。6.2 中文标签显示乱码cv2.putText不支持中文写中文会显示成一串问号。这是个老问题解决办法有几种一是用英文或拼音代替二是用 PIL 库绘制文字再转回 OpenCV 格式三是提前把文字渲染成图片贴上去。如果项目里必须显示中文我一般用第二种虽然多引入一个依赖但最灵活。6.3 图像通道顺序导致的颜色错乱OpenCV 读图是 BGR而很多其他库比如 matplotlib显示时按 RGB 解释。如果你用 matplotlib 显示 OpenCV 读的图红色和蓝色会对调。画框时如果颜色定义来自 RGB 体系也会出现同样的错乱。统一原则在 OpenCV 体系内一律用 BGR需要跨库时显式转换。6.4 常见问题速查表现象可能原因排查方向框位置整体偏移坐标格式混淆确认是 xywh 还是 xyxy框大小不对缩放比例未同步检查图像与坐标是否同比例变换颜色和预期不符BGR/RGB 混用统一颜色空间文字显示为问号不支持中文换英文或用 PIL 渲染批处理中途崩溃未判空/未捕获异常加判空和 try 包裹标注与图不对应文件名匹配错误检查文件与标注的对应关系6.5 几个我踩过的坑第一个坑是用img.shape[0]当宽度。这个错误在代码里非常隐蔽因为两个值都是整数不会报错只是框画出来比例不对。后来我强制自己写h, w img.shape[:2]并且变量名绝不简写成单个字母以外的东西减少混淆。第二个坑是在循环里反复imread同一张图。早期写代码没注意一个函数里读了三遍同一张图数据量一大就明显变慢。后来改成读一次传引用速度提升很明显。第三个坑是忽略图像方向信息。有些手机拍的图带有旋转元数据imread读进来可能是旋转过的导致标注坐标和实际显示对不上。处理这类图时需要先根据元数据做方向校正。7. 从尺寸到标注一个完整工作流的串接把前面所有内容串起来一个典型的图像标注工作流是这样的先扫描目录统计尺寸分布筛掉异常图然后对每张图读取宽高拿到标注框对框做边界裁剪和格式转换绘制可视化结果用于人工抽检最后把归一化后的标注写入结构化文件供下游使用。这个流程里宽高获取是最基础的一环但它贯穿始终——裁剪要用、归一化要用、绘制自适应线宽也要用。所以把它写成一个独立、健壮、可复用的函数是整个项目质量的第一道保障。我现在的习惯是每个视觉项目都先建一个image_utils模块把尺寸获取、格式转换、绘制这些基础操作放进去后面所有代码都从这里调用避免重复造轮子和各处实现不一致。实际用下来这套基础打得越扎实后面接模型、做训练、跑推理时出的问题就越少。很多看似复杂的 bug追根溯源都是坐标、尺寸、通道这些最基础的东西没处理好。把这一层做对后面的路会顺很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑