上周帮同事看一个滤波 demo,他甩过来一张截图,说"图像边缘怎么多了一圈脏东西"。代码里滤波核是 5×5,图像尺寸前后没变,但四周一圈像素颜色明显发暗。问题既不在核,也不在图像本身,而在于 OpenCV 处理边界时悄悄做了一件事 —— 它得给越界的像素"补一个值",默认补法就是镜像复制。他不知道这件事在发生,自然也想不到去控制它。这个"补值"动作,在 OpenCV 里有个专门的名字叫图像加边框,对应函数是 cv2.copyMakeBorder。OpenCV 为图像添加边框看着是个一行代码的小功能,参数也就那么几个,但要用稳、用对,里面有一堆值得单独拎出来讲的门道:六种边界模式到底差在哪一个像素、加完边框之后标注框坐标怎么跟着走、跟 numpy 手写填充比谁快、透明 PNG 会不会翻车。不管你是刚装完 opencv-python 想跑通第一个例程,还是在做图像算法、目标检测、文字识别、图像拼接这类活儿,把这套东西理清楚都会省下不少返工时间。
1. 加边框到底在解决什么问题:四类场景决定你选哪种填充
1.1 卷积滤波时,边界像素的"续命"机制
先说最容易被忽略的一类。卷积核在图像上滑窗,滑到左上角的时候,核有一大半已经跑到图像外面去了,那些位置没有真实像素。要么跳过这些位置(输出尺寸变小),要么凭空造出一些像素来参与计算(输出尺寸不变)。OpenCV 的 filter2D、GaussianBlur、Sobel、erode、dilate 这些函数全都默认走第二条路,边框类型用的是 BORDER_DEFAULT,也就是 BORDER_REFLECT_101。
这意味着什么呢,意味着你对一张图做任何带核的操作,边缘那几圈像素的值,是被"镜像出来的假像素"参与计算出来的,跟图像内部像素的计算依据完全不同。5×5 的核,最外圈像素受假像素影响最大,往内两圈逐渐减弱。这就是同事那张截图里暗边的来源 —— 图像本身四周是浅色背景,镜像复制之后假像素也算浅色,但核里带了负数权重,边界处正负抵消不完全,就压出了一圈灰。
所以当你发现滤波结果边缘异常、Sobel 边缘检测在图像四边画出一圈虚假轮廓、形态学操作把边缘吃掉一圈时,第一反应应该是去看 borderType,而不是怀疑核写错了。
1.2 尺寸不一的图像要拼到同一张画布
第二类场景是做图像拼接或者结果可视化。手上有几张分辨率不同的图,想横向排成一行展示,最省事的做法不是 resize(会把内容拉伸变形),而是先算出最大高度,给每张矮图上下补边框凑齐,再横向拼。摄像头采集、多路视频对比、模型输入输出并排看效果,都会用到这一招。
这种场景下填充色的选择很关键。做对比展示一般用白色或者中灰,视觉上不会干扰主体;如果是做模型输入的 letterbox,常用的是 (114, 114, 114) 这个灰值,因为它在常见的归一化流程里对应 0.447 左右的均值,不会给网络引入额外的偏置。
1.3 贴边文字和标注框需要安全边距
第三类是做 OCR 和文字识别。很多识别模型对贴边的文字很不友好,因为文字笔画顶到图像边界,做池化和卷积之后特征被截断,识别率会往下掉。一个几乎零成本的技巧是给原图加一圈白边,宽度取字高的 0.3 到 0.5 倍,识别率通常能涨几个点。这个技巧我在票据和证件识别里试过,稳定有效。
同样的道理适用于目标检测的数据增强和推理预处理。目标框贴着图像边缘时,做随机裁剪或者缩放很容易把目标切掉一半,加一圈 padding 相当于给标注框买了份保险。
1.4 打印排版与视觉留白
第四类是非技术场景。做海报、做展板、把图表导出成图片时,图形元素直接顶到画布边缘会显得很挤,留白本身就是设计语言的一部分。用代码批量给一批图统一加等宽白边,比一张张开 Photoshop 改画布尺寸高效得多,而且能保证每张图的留白完全一致。
这四类场景对应的填充模式其实不一样:滤波要的是镜像,拼图要的是常数,文字识别要的是常数白边,视觉留白要的也是常数。把场景和模式对上号,比死记硬背常量名有用得多。
2. copyMakeBorder 的参数逐个拆:六个边界常量到底差在哪
2.1 函数签名与每个位置参数的含义
Python 里的调用形式长这样:
import cv2 dst = cv2.copyMakeBorder( src, # 输入图像,numpy 数组 top, # 上方填充像素数,int bottom, # 下方填充像素数 left, # 左侧填充像素数 right, # 右侧填充像素数 borderType, # 边界模式,cv2.BORDER_* 常量 value=None # 仅 BORDER_CONSTANT 生效的填充值 )七个参数,前五个是位置参数,后面两个有默认值。四个方向的填充量可以各不相同,这点在拼图时特别有用:想让图像在画布上偏左一点,就把 right 给大一些,不需要额外做坐标偏移。
value 参数只在 borderType 为 BORDER_CONSTANT 时起作用,其余模式下传了也会被忽略,不会报错,这点容易让人误以为填充色没生效,其实是用错了模式。value 的颜色顺序是 BGR,跟 OpenCV 读图的通道顺序保持一致,单通道灰度图直接传一个整数就行,三通道传三元组,四通道传四元组。
调用完之后,输出图像会是一块新分配的内存,形状为 (h+top+bottom, w+left+right, channels),dtype 跟输入一致。想看这块内存是不是连续的,可以打印 dst.flags['C_CONTIGUOUS'],绝大多数情况下是 True。
2.2 六种 borderType 的像素级对照
OpenCV 定义的边界模式常量有这么几个,用一行像素 abcdefgh 来表示原图边缘附近的内容,各模式的填充结果如下:
| 常量名 | 填充规则示意 | 一句话理解 |
|---|---|---|
| BORDER_CONSTANT | iiiiii|abcdefgh|iiiiiii | 补固定值 i |
| BORDER_REPLICATE | aaaaaa|abcdefgh|hhhhhhh | 复制最边缘的像素 |
| BORDER_REFLECT | fedcba|abcdefgh|hgfedcb | 以边缘像素为轴镜像,边缘被复制一次 |
| BORDER_REFLECT_101 | gfedcb|abcdefgh|gfedcba | 以边缘像素为轴镜像,边缘不重复 |
| BORDER_WRAP | cdefgh|abcdefgh|abcdefg | 从对面绕过来接着填 |
| BORDER_DEFAULT | 同 BORDER_REFLECT_101 | 上面那个的别名 |
BORDER_REFLECT 和 BORDER_REFLECT_101 这两个名字里的 101 是最容易搞混的地方。101 的意思是"1 和 0 都不重复",也就是镜像轴正好落在最外侧那个像素和它外侧的虚拟位置之间的中点,导致边缘像素不会出现两次。而 BORDER_REFLECT 的镜像轴落在边缘像素自己身上,边缘像素就在两侧各出现一次。
WARP 模式在实际项目里用得少,但在做纹理平铺、周期性图案合成时很有用,因为它保证填充出来的图和原图能无缝首尾相接。
2.3 value 参数受 dtype 约束,别乱填
value 的取值会按输入图像的数据类型做转换。图像是 uint8 的时候,合法范围是 0 到 255,填个 255 出来就是纯白;如果填了 300 这种越界值,结果不会像你想象的那样"亮度拉满",而是会出现不直观的回绕行为。这不是 OpenCV 的 bug,是它按底层位宽截断的结果,所以别指望用超范围的值来做调试标记。
另一个容易忽略的点是浮点图像。如果你在做光照归一化、HDR 合成,图像已经是 float32 或 float64,value 就要按浮点范围给,比如归一化到 0 到 1 的图像应该填 1.0 而不是 255。混着填会出现要么全黑、要么全白的极端结果。
多通道时,value 的长度要么等于通道数,要么给单值让所有通道统一。传了三个值给四通道的 BGRA 图,OpenCV 会怎么处理?实测表现不稳定,最好还是老老实实给够四个分量。
2.4 一像素之差带来的实际影响
REFLECT 和 REFLECT_101 这一像素的区别,在大多数视觉任务里肉眼看不出来,但在两个地方会放大:一是做频域分析或者求导运算,边缘重复一次会导致梯度计算出现零点跳变;二是做数据增强时反复叠加镜像翻转,重复的边缘像素会让模型对边缘特征的统计量产生偏差。OpenCV 自己把 BORDER_DEFAULT 定成 REFLECT_101 而不是 REFLECT,也是出于这个考虑 —— 不重复边缘,统计性质更干净。
所以除非你有特别的理由,做滤波、做形态学、做 Sobel,用默认值就行。想手工指定的话,直接写 cv2.BORDER_REFLECT_101,比写 BORDER_DEFAULT 更能让看代码的人明白你在干什么。
3. 一段可以逐行复现的验证代码:把六种模式摆在一起看
3.1 造一张边界清晰的测试图
要验证行为,先用一张能一眼看出边界的测试图。别拿照片,照片本身纹理复杂,看不出填充是从哪儿开始的。用代码造:
import cv2 import numpy as np h, w = 6, 8 img = np.zeros((h, w, 3), np.uint8) img[:] = (40, 40, 40) # 中间是深灰 img[0, :] = (0, 0, 255) # 上边一行为红 img[-1, :] = (0, 255, 0) # 下边一行为绿 img[:, 0] = (255, 0, 0) # 左边一列为蓝 img[:, -1] = (0, 255, 255) # 右边一列为黄 img[1, 1] = (255, 255, 255) # 左上角内侧放一个白点做锚点 print(img.shape, img.dtype) # (6, 8, 3) uint8这张图只有 6×8,每个像素都能在打印出来的数值里找到,方便逐格核对。填 2 个像素宽就足够看出模式差异了,填太大反而不好数。
3.2 六种模式拼成一张对比图
border_types = [ ("CONSTANT", cv2.BORDER_CONSTANT), ("REPLICATE", cv2.BORDER_REPLICATE), ("REFLECT", cv2.BORDER_REFLECT), ("REFLECT_101", cv2.BORDER_REFLECT_101), ("WRAP", cv2.BORDER_WRAP), ] PAD = 2 rows = [] for name, bt in border_types: out = cv2.copyMakeBorder( img, PAD, PAD, PAD, PAD, borderType=bt, value=(128, 128, 128) # 只有 CONSTANT 会用到 ) out = cv2.resize(out, None, fx=16, fy=16, interpolation=cv2.INTER_NEAREST) out = cv2.copyMakeBorder(out, 24, 4, 4, 4, cv2.BORDER_CONSTANT, value=(255, 255, 255)) cv2.putText(out, name, (6, 18), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) rows.append(out) cv2.imwrite("border_compare.png", np.vstack(rows))这里用 INTER_NEAREST 放大 16 倍,是为了保持像素块锐利,不被插值糊掉,能一格一格数清楚。放大之后又加了一圈白边并写上模式名,方便对照。如果你在 notebook 里跑,直接把 np.vstack(rows) 用 cv2.cvtColor 转成 RGB 显示出来更省事。
3.3 用 numpy.pad 反向验证填充结果
想确认自己对填充规则的理解对不对,可以用 numpy.pad 做交叉验证。两者的模式名对应关系很容易记混,我把对照整理成表:
| OpenCV 常量 | numpy.pad mode | 说明 |
|---|---|---|
| BORDER_CONSTANT | 'constant' | 需配合 constant_values |
| BORDER_REPLICATE | 'edge' | 复制边缘 |
| BORDER_REFLECT | 'symmetric' | 边缘重复 |
| BORDER_REFLECT_101 | 'reflect' | 边缘不重复 |
| BORDER_WRAP | 'wrap' | 环绕 |
验证代码:
a = cv2.copyMakeBorder(img, 2, 2, 2, 2, cv2.BORDER_REFLECT_101) b = np.pad(img, ((2, 2), (2, 2), (0, 0)), mode='reflect') print(np.array_equal(a, b)) # True c = cv2.copyMakeBorder(img, 2, 2, 2, 2, cv2.BORDER_REFLECT) d = np.pad(img, ((2, 2), (2, 2), (0, 0)), mode='symmetric') print(np.array_equal(c, d)) # True注意 numpy 里 'reflect' 对应的是 OpenCV 的 REFLECT_101,'symmetric' 才对应 REFLECT,这两个名字的直觉和实际行为是反的,我见过不少人栽在这里,调了半天以为 OpenCV 算错了。
3.4 把填充区域染色,直观确认边框位置
还有一种可视化技巧:先按目标模式做一次填充,然后手工把填充区域涂成一个醒目的颜色,看一眼就知道边界在哪儿。
def show_padding(img, top, bottom, left, right): out = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_REFLECT_101) h, w = img.shape[:2] mask = np.ones(out.shape[:2], bool) mask[top:top+h, left:left+w] = False # 原图区域标记为 False out[mask] = (0, 0, 255) # 填充区域染红 return out这个函数在写自己的 padding 逻辑时特别顺手,能立刻发现"左边多填了一行""上下不对称"这类坐标算错的问题,比盯着数字调快得多。
4. 加完边框之后的坐标账:尺寸、ROI 和标注框怎么跟着动
4.1 尺寸与原点的变化公式
加边框这件事本身很简单,真正容易出错的是加完之后所有坐标都要跟着平移。基本关系:
- 新宽度 = 原宽度 + left + right
- 新高度 = 原高度 + top + bottom
- 原图中坐标为 (x, y) 的点,在新图中的坐标变成 (x + left, y + top)
看着没什么,但在一个流程里同时涉及原图坐标、模型输入坐标、可视化坐标时,漏掉一次平移就是灾难。我在做一个多路摄像头拼接的项目时,因为忘了把检测框坐标加上左侧 padding,画出来的框整整偏了几十像素,排查了快一个小时才发现是这一行。
4.2 用 ROI 把图像放到画布中央
如果只是想把一张图放到固定尺寸的画布里居中,不一定非得用 copyMakeBorder,直接建画布再赋值 ROI 更灵活:
def paste_center(img, canvas_size=(640, 640), bg=255): ch, cw = canvas_size h, w = img.shape[:2] if h > ch or w > cw: scale = min(ch / h, cw / w) img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) h, w = img.shape[:2] top = (ch - h) // 2 left = (cw - w) // 2 canvas = np.full((ch, cw, 3), bg, np.uint8) canvas[top:top+h, left:left+w] = img return canvas, (left, top)注意这里返回的 (left, top),就是后续把画布坐标映射回原图坐标的偏移量。养成"谁做变换谁返回参数"的习惯,比事后满世界找常量靠谱。另外算 top 和 left 时用的是整除,画布和图的尺寸差是奇数时会有一像素的不对称,这是正常的,不必强求。
4.3 目标检测标注框的同步偏移
做数据增强或者推理预处理时,如果对图像做了 padding,标注框必须同步平移:
def shift_boxes(boxes, left, top): """boxes: [[x1, y1, x2, y2], ...] 绝对坐标""" return [[x1 + left, y1 + top, x2 + left, y2 + top] for x1, y1, x2, y2 in boxes]如果是 YOLO 那种归一化坐标,就要先把 padding 后的新宽高新高一并算进去再归一化,不能只加偏移。这一步在很多开源预处理的 letterbox 函数里都有体现,比如常见的做法是先把长边缩放到目标尺寸,短边用 copyMakeBorder 补灰边,同时记录缩放比例和填充量,推理完把框映射回去时反向操作一遍:
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): h, w = img.shape[:2] r = min(new_shape[0] / h, new_shape[1] / w) new_w, new_h = int(round(w * r)), int(round(h * r)) dw, dh = (new_shape[1] - new_w) / 2, (new_shape[0] - new_h) / 2 if (w, h) != (new_w, new_h): img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img, r, (left, top)那个 int(round(x - 0.1)) 和 int(round(x + 0.1)) 的写法看着别扭,其实是为了让上下(或左右)两边的填充量在临界情况下取整一致,避免出现一边多一像素导致目标中心整体偏移半像素的微妙问题。这个细节在几个主流检测框架的预处理里都能看到,算是个流传很广的工程惯例。
4.4 反推:想让卷积输出尺寸不变,padding 该给多少
如果不用 OpenCV 的自动边界处理,自己控制 padding,那么保持输出尺寸不变的条件是:
对于 stride 为 1、方形核大小为 k 的卷积,padding = (k - 1) / 2。k 是奇数时刚好整数,3×3 补 1,5×5 补 2,7×7 补 3。通用公式是 out = floor((in + 2p - k) / s) + 1,把 out = in 代进去解 p 就行。
用 copyMakeBorder 先补边,再把 filter2D 的 borderType 设成 BORDER_CONSTANT 且 value 给 0,其实等价于手工控制边界值。这个技巧在复现论文里的卷积细节时很有用,因为论文多数默认零填充,而 OpenCV 默认是镜像填充,两边对不上结果就差了。
5. copyMakeBorder 与 numpy 手写填充:性能、内存和适用边界
5.1 实测速度差多少
写个简单的计时对比,在 1080p 三通道图像上各做 200 次:
import time img = cv2.imread("test.jpg") assert img is not None t0 = time.perf_counter() for _ in range(200): a = cv2.copyMakeBorder(img, 32, 32, 32, 32, cv2.BORDER_REFLECT_101) t1 = time.perf_counter() t2 = time.perf_counter() for _ in range(200): b = np.pad(img, ((32, 32), (32, 32), (0, 0)), mode='reflect') t3 = time.perf_counter() print(f"copyMakeBorder: {(t1-t0)*1000:.1f} ms") print(f"np.pad: {(t3-t2)*1000:.1f} ms") print("shape equal:", a.shape == b.shape)我本地跑下来 copyMakeBorder 通常比 np.pad 快两到五倍,图越大差距越明显。原因是 copyMakeBorder 底层走了 SIMD 优化的拷贝路径,而 np.pad 在内部要处理通用的多轴逻辑,分支判断多,还容易触发额外的内存分配。批量处理几千张图的时候,这个差距会累积成实打实的等待时间。
5.2 numpy 方式什么时候反而更合适
但 np.pad 也不是没有用武之地。有几种情况我会主动选它:
一是需要非对称、逐轴不同的填充时,np.pad 的 pad_width 参数一次就能写完,比如 ((2, 4), (3, 5), (0, 0)),比调 copyMakeBorder 再手动拼接清爽。二是处理的目标是 numpy 数组而不是图像语义,比如特征图、热力图、掩码,本来就没有图像这个概念,用 np.pad 更自然。三是在做单元测试时,用 np.pad 作为参考实现去验证自己封装的 padding 函数,交叉验证比自证有说服力。
还有一种是维度大于三维的数据,比如视频张量 (T, H, W, C) 或者批量图像 (N, H, W, C),np.pad 直接给 pad_width 加一行就行,copyMakeBorder 只能处理单张图,得写循环。
5.3 不连续矩阵、ROI 视图与大图内存
copyMakeBorder 对输入矩阵的内存布局没有要求,传进去的即使是从大图切出来的 ROI 视图(不连续),它也能正常处理,输出是重新分配的连续内存。这一点挺省心的,不像有些函数遇到不连续输入会静默出错或者异常慢。
真正需要注意的是内存。假设你在处理 4K 图像,尺寸 3840×2160,三通道 uint8 大概 24 MB;如果填充 512 像素,输出变成 4864×3200,接近 47 MB,一次翻倍。批量处理时如果一次性把所有结果都留在列表里,几百张就能把内存顶爆。我的做法是处理完立刻落盘或者喂给下游,中间结果该 del 就 del,别攒着。
还有个小坑:如果你在循环里对同一个变量反复做 copyMakeBorder,比如每轮补 2 像素迭代 100 次,规模是指数增长的,跑几个循环就可能卡死。真有这种需求,改成一次性算好总填充量,一次调用搞定。
6. 报错与异常:从提示信息倒推真实原因
6.1 ModuleNotFoundError 与 opencv-python 的安装姿势
最常见的一类报错是:
ModuleNotFoundError: No module named 'cv2'它的意思非常直白,就是当前 Python 环境里没装 OpenCV 的 Python 绑定。要注意的是,装了 opencv-python 才是 import cv2,包名和导入名不一样,这点对新手很不友好。安装命令:
pip install opencv-python如果你需要 SIFT、SURF 这些曾经进过专利保护区的算法,得装 opencv-contrib-python;如果只是基本读写和 copyMakeBorder,用 opencv-python 就够了,包体积也小。
Anaconda 用户经常会碰到"明明在 Anaconda Prompt 里 pip 装成功了,换个终端 import 还是失败"的情况,根源是装了多个 Python 环境,pip 和 python 指向的不是同一个。先用:
python -c "import sys; print(sys.executable)" pip -V把两个路径打出来对比一下,基本上一眼就能看出问题。确认环境之后,稳妥的做法是在目标环境里用python -m pip install opencv-python,强制让 pip 跟随当前解释器。
6.2 BORDER_TRANSPARENT 直接用会抛错
OpenCV 里有个 BORDER_TRANSPARENT 常量,名字听起来像是"不填充",但它不是给 copyMakeBorder 用的,它只是滤波类函数内部用来标记某类特殊行为的占位值。把它传给 copyMakeBorder,会直接抛 cv2.error,报错信息大意是边界类型不被支持。这个坑我在第一次看到这个常量名的时候也踩过。
想实现"不填充",正确做法是自己用 numpy 建一块透明或指定颜色的画布,再用 ROI 赋值把原图贴上去,也就是 4.2 节那个函数的路子。
6.3 四通道 PNG 与 alpha 边框
带透明通道的 PNG,用 cv2.imread 默认参数读进来会丢掉 alpha,变成三通道 BGR。想保留透明度必须显式指定:
img = cv2.imread("logo.png", cv2.IMREAD_UNCHANGED) print(img.shape) # 可能是 (h, w, 4)四通道图做 copyMakeBorder 时,value 要给四个分量:
out = cv2.copyMakeBorder(img, 20, 20, 20, 20, cv2.BORDER_CONSTANT, value=(255, 255, 255, 0))四个分量依次是蓝、绿、红、alpha。想把边框做成完全透明的,alpha 给 0;想做不透明白边,alpha 给 255。这一处如果给错了,出来的图要么边框颜色诡异,要么在支持透明的查看器里显示成一整块黑边,实机上很容易误判成"程序写错了"。
另外提醒一句,四通道图后续如果要做颜色空间转换、边缘检测,大多不能直接用,需要先分离 alpha 通道单独处理,或者转成三通道。这个转换点放在加边框之前还是之后,取决于你的实际需求,但一定要想清楚,别在中间糊里糊涂地丢通道。
6.4 中文路径读图返回 None 的连锁反应
cv2.imread 在 Windows 上遇到含中文的路径,会安静地返回 None,不抛异常。后续所有操作就会在 None 上报错,而报错信息往往指向 copyMakeBorder 或者说 src 不是 numpy 数组,让人误以为是 padding 的问题。排查方法很简单,读图之后立刻断言:
img = cv2.imread(path) if img is None: raise FileNotFoundError(f"读图失败: {path}")彻底绕开的办法是用 np.fromfile 配合 cv2.imdecode:
import numpy as np def imread_unicode(path, flags=cv2.IMREAD_COLOR): data = np.fromfile(path, dtype=np.uint8) return cv2.imdecode(data, flags)这个封装我几乎每个项目都留一份,能省掉很多莫名其妙的排查时间。写图的场景同理,用 cv2.imencode 加 tofile 处理中文路径。
7. 三个我把边框用进项目的实际例子
第一个是票据识别的前处理。票据扫描件里,文字经常紧贴纸张边缘,直接送进识别模型时最外圈字符的准确率明显低一截。我在缩放之前统一加了一圈白边,宽度按图像短边的 2% 取,识别率在测试集上稳定提升了两三个百分点。这个改动只是几行代码,但比调模型参数见效快多了。
第二个是模型结果的可视化。做分割任务时,预测掩码和原图尺寸一致,但我想把原图和掩码上下叠在一起展示,中间还要留出标注空间。做法是先给原图底部加一条白边,把文字说明画在里边,再把掩码转成伪彩色拼到下面。整个过程就是两次 copyMakeBorder 加一次 vconcat,比拼图工具方便太多。
第三个是训练数据的合成。做工业质检的小样本任务,我需要在有限的原图上合成更多样本,做法是把缺陷区域抠出来,贴到不同背景上,贴的位置随机。这里用 ROI 赋值比 copyMakeBorder 灵活,但在生成背景画布的时候,会用 copyMakeBorder 把小块纹理铺满整个画布,BORDER_WRAP 模式正好适合这种需要无缝延展的场景,比手动平铺少写一层循环。
回过头看,图像加边框这件事,功能本身只有一行代码,但要把边界模式选对、把坐标账算清、把 dtype 和通道数对齐、把报错信息看明白,还是需要一点耐心。我在实际操作中的体会是,凡是要在图像边缘做手脚的地方,先把"这块像素到底从哪来"这个问题问清楚,后面基本都不会跑偏。