OpenCV copyMakeBorder 图像边界填充教程:原理、三种语言实现与底层源码解析
copyMakeBorder 是 OpenCV 中用于为图像四周扩展(填充)边界的核心函数,它既是卷积、滤波等运算处理图像边缘像素时不可或缺的前置步骤,也是为图像添加相框式装饰(Constant 彩色边框、Replicate 复制边缘)的常用工具。本文以官方教程《Adding borders to your images》为主线,结合本仓库中的 C++/Python/Java 示例程序与 imgproc/core 模块源码,深入讲解 borderType 各模式的语义、演示程序的完整逻辑,以及函数在 ROI、OpenCL/IPP 加速下的底层实现。读完本文,你将能独立使用 copyMakeBorder 实现任意尺寸、任意模式的图像边框填充,并理解 OpenCV 滤波函数“先扩边、再卷积、后裁剪”的经典边界处理套路。
目标:学会什么
本教程聚焦一个核心 API:
- 使用 OpenCV 函数
copyMakeBorder()为图像设置边界(即额外的填充像素)。
配套交互演示程序覆盖了两种最常用填充模式,用户通过按键实时切换:
| 按键 | 行为 |
|---|---|
c |
切换到 Constant value border(恒定值边框):为整圈边界填充一个恒定颜色,该颜色每隔 0.5 秒随机刷新一次 |
r |
切换到 Replicated border(复制边框):边界由原图像边缘像素直接复制而来 |
ESC |
退出程序 |
为什么需要边界填充:卷积的边界难题
本节理论说明来自 Bradski 与 Kaehler 所著的《Learning OpenCV》。
- 在上一教程(图像卷积操作(filter2D))中我们学习了用卷积处理图像。随之而来的一个天然问题是:如何处理边界? 当被计算的点位于图像边缘时,卷积核的一部分会滑出图像范围,我们拿什么像素去参与运算?
- 大多数 OpenCV 函数的做法是:先把给定图像复制到一块略大的画布上,再按某种模式自动补出边界(模式即下文示例代码中列出的若干种)。这样卷积就能在所需像素上顺利进行,运算完成后把多余的填充区域裁掉即可。
- 本教程将重点探索两种定义边界(padding)的方式:
BORDER_CONSTANT:用恒定值填充边界(如黑色,即数值 0);BORDER_REPLICATE:把原图像最边缘的那一行/列像素复制到外部边界上。
在 Code 部分你会更直观地看到这两种模式的区别。
认识 API:函数原型与完整 BorderTypes 家族
copyMakeBorder 在 modules/core/include/opencv2/core.hpp 中声明,核心签名如下:
CV_EXPORTS_W void copyMakeBorder(InputArray src, OutputArray dst,
int top, int bottom, int left, int right,
int borderType, const Scalar& value = Scalar() );
参数含义:
| 参数 | 说明 |
|---|---|
src |
源图像 |
dst |
目标图像,与 src 同类型,尺寸为 Size(src.cols + left + right, src.rows + top + bottom) |
top / bottom / left / right |
各方向需要外扩的像素数。例如 top=1, bottom=1, left=1, right=1 表示四周各生成 1 像素宽的边界 |
borderType |
边界外推类型,取值为 BorderTypes 中的一种(详细见下),细节语义与 borderInterpolate 一致 |
value |
当 borderType == BORDER_CONSTANT 时用于填充边界的像素值 |
除了教程中的两种模式,OpenCV 还提供了更多边界外推策略。它们统一定义在 modules/core/include/opencv2/core/base.hpp 的枚举 BorderTypes 中,注释中的 abcdefgh 示意了以 8 像素图像为原、两侧外扩的像素取值规律:
| 枚举值 | 数值 | 外推规则示意 | 说明 |
|---|---|---|---|
BORDER_CONSTANT |
0 | `iiiiii | abcdefgh |
BORDER_REPLICATE |
1 | `aaaaaa | abcdefgh |
BORDER_REFLECT |
2 | `fedcba | abcdefgh |
BORDER_WRAP |
3 | `cdefgh | abcdefgh |
BORDER_REFLECT_101 |
4 | `gfedcb | abcdefgh |
BORDER_TRANSPARENT |
5 | `uvwxyz | abcdefgh |
BORDER_REFLECT101 / BORDER_DEFAULT |
4 的别名 | 同上 | 便于阅读的兼容写法 |
BORDER_ISOLATED |
16 | — | 标志位(按位或组合使用),将外推严格限制在 ROI 内部,不允许引用 ROI 之外的母图像素 |
需要特别说明 BORDER_ISOLATED:当 src 只是某张大图的 ROI(子区域)时,函数默认会尽量借用 ROI 之外的母图真实像素来构成边界;若希望关闭这一行为、始终执行外推,则需要传入 borderType | BORDER_ISOLATED。这一细节在交互演示中并不直观,但在编写 ROI 上的滤波/填充逻辑时非常关键,后文源码部分会给出验证。
演示程序代码
官方演示代码保存在本仓库 samples 目录下,三份代码功能完全等价:
- C++:samples/cpp/tutorial_code/ImgTrans/copyMakeBorder_demo.cpp
- Python:samples/python/tutorial_code/ImgTrans/MakeBorder/copy_make_border.py
- Java:samples/java/tutorial_code/ImgTrans/MakeBorder/CopyMakeBorder.java
C++ 完整代码
/**
* @file copyMakeBorder_demo.cpp
* @brief Sample code that shows the functionality of copyMakeBorder
* @author OpenCV team
*/
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
// Declare the variables
Mat src, dst;
int top, bottom, left, right;
int borderType = BORDER_CONSTANT;
const char* window_name = "copyMakeBorder Demo";
RNG rng(12345);
int main( int argc, char** argv )
{
const char* imageName = argc >=2 ? argv[1] : "lena.jpg";
// Loads an image
src = imread( samples::findFile( imageName ), IMREAD_COLOR ); // Load an image
// Check if image is loaded fine
if( src.empty()) {
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default lena.jpg] \n");
return -1;
}
// Brief how-to for this program
printf( "\n \t copyMakeBorder Demo: \n" );
printf( "\t -------------------- \n" );
printf( " ** Press 'c' to set the border to a random constant value \n");
printf( " ** Press 'r' to set the border to be replicated \n");
printf( " ** Press 'ESC' to exit the program \n");
namedWindow( window_name, WINDOW_AUTOSIZE );
// Initialize arguments for the filter
top = (int) (0.05*src.rows); bottom = top;
left = (int) (0.05*src.cols); right = left;
for(;;)
{
Scalar value( rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255) );
copyMakeBorder( src, dst, top, bottom, left, right, borderType, value );
imshow( window_name, dst );
char c = (char)waitKey(500);
if( c == 27 )
{ break; }
else if( c == 'c' )
{ borderType = BORDER_CONSTANT; }
else if( c == 'r' )
{ borderType = BORDER_REPLICATE; }
}
return 0;
}
Python 完整代码
"""
@file copy_make_border.py
@brief Sample code that shows the functionality of copyMakeBorder
"""
import sys
from random import randint
import cv2 as cv
def main(argv):
# First we declare the variables we are going to use
borderType = cv.BORDER_CONSTANT
window_name = "copyMakeBorder Demo"
imageName = argv[0] if len(argv) > 0 else 'lena.jpg'
# Loads an image
src = cv.imread(cv.samples.findFile(imageName), cv.IMREAD_COLOR)
# Check if image is loaded fine
if src is None:
print ('Error opening image!')
print ('Usage: copy_make_border.py [image_name -- default lena.jpg] \n')
return -1
# Brief how-to for this program
print ('\n'
'\t copyMakeBorder Demo: \n'
' -------------------- \n'
' ** Press \'c\' to set the border to a random constant value \n'
' ** Press \'r\' to set the border to be replicated \n'
' ** Press \'ESC\' to exit the program ')
cv.namedWindow(window_name, cv.WINDOW_AUTOSIZE)
# Initialize arguments for the filter
top = int(0.05 * src.shape[0]) # shape[0] = rows
bottom = top
left = int(0.05 * src.shape[1]) # shape[1] = cols
right = left
while 1:
value = [randint(0, 255), randint(0, 255), randint(0, 255)]
dst = cv.copyMakeBorder(src, top, bottom, left, right, borderType, None, value)
cv.imshow(window_name, dst)
c = cv.waitKey(500)
if c == 27:
break
elif c == 99: # 99 = ord('c')
borderType = cv.BORDER_CONSTANT
elif c == 114: # 114 = ord('r')
borderType = cv.BORDER_REPLICATE
return 0
if __name__ == "__main__":
main(sys.argv[1:])
注意 Python 版的函数签名与 C++ 不同,边界参数顺序为 (src, top, bottom, left, right, borderType[, dst[, value]]),需要显式传入 None 作为 dst 占位;按键判断使用的是 ASCII 码 99('c')与 114('r')。
Java 完整代码
/**
* @file CopyMakeBorder.java
* @brief Sample code that shows the functionality of copyMakeBorder
*/
import org.opencv.core.*;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import java.util.Random;
class CopyMakeBorderRun {
public void run(String[] args) {
// Declare the variables
Mat src, dst = new Mat();
int top, bottom, left, right;
int borderType = Core.BORDER_CONSTANT;
String window_name = "copyMakeBorder Demo";
Random rng;
String imageName = ((args.length > 0) ? args[0] : "../data/lena.jpg");
// Load an image
src = Imgcodecs.imread(imageName, Imgcodecs.IMREAD_COLOR);
// Check if image is loaded fine
if( src.empty() ) {
System.out.println("Error opening image!");
System.out.println("Program Arguments: [image_name -- default ../data/lena.jpg] \n");
System.exit(-1);
}
System.out.println("\n" +
"\t copyMakeBorder Demo: \n" +
"\t -------------------- \n" +
" ** Press 'c' to set the border to a random constant value \n" +
" ** Press 'r' to set the border to be replicated \n" +
" ** Press 'ESC' to exit the program \n");
HighGui.namedWindow( window_name, HighGui.WINDOW_AUTOSIZE );
top = (int) (0.05*src.rows()); bottom = top;
left = (int) (0.05*src.cols()); right = left;
while( true ) {
rng = new Random();
Scalar value = new Scalar( rng.nextInt(256),
rng.nextInt(256), rng.nextInt(256) );
Core.copyMakeBorder( src, dst, top, bottom, left, right, borderType, value);
HighGui.imshow( window_name, dst );
char c = (char) HighGui.waitKey(500);
c = Character.toLowerCase(c);
if( c == 27 )
{ break; }
else if( c == 'c' )
{ borderType = Core.BORDER_CONSTANT;}
else if( c == 'r' )
{ borderType = Core.BORDER_REPLICATE;}
}
System.exit(0);
}
}
public class CopyMakeBorder {
public static void main(String[] args) {
// Load the native library.
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new CopyMakeBorderRun().run(args);
}
}
Java 版通过 HighGui.namedWindow/imshow/waitKey 复用 C++ 的窗口体系,按键统一转为小写后比较,因此 C 与 c 均可切换恒定值模式。
程序逻辑逐段拆解
声明变量
C++ 版声明了两个全局图像 Mat src, dst、四个边界尺寸变量以及边框类型:
Mat src, dst;
int top, bottom, left, right;
int borderType = BORDER_CONSTANT;
const char* window_name = "copyMakeBorder Demo";
RNG rng(12345);
其中需要特别关注的是变量 rng——一个随机数发生器(默认种子 12345),我们很快会用它生成随机的边框颜色。C++ 使用 cv::RNG;Java 使用 java.util.Random;Python 则直接调用标准库 random.randint。
加载图像
与绝大多数 OpenCV 示例一致,从命令行参数(缺省为 lena.jpg)读取源图像,并通过 cv::samples::findFile(C++)或 cv.samples.findFile(Python)在已注册的样本数据目录中查找该文件:
const char* imageName = argc >=2 ? argv[1] : "lena.jpg";
src = imread( samples::findFile( imageName ), IMREAD_COLOR );
if( src.empty()) {
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default lena.jpg] \n");
return -1;
}
创建窗口与打印提示
程序先用 printf/print 打印操作说明,再创建窗口:
namedWindow( window_name, WINDOW_AUTOSIZE );
WINDOW_AUTOSIZE 表示窗口尺寸自动适配图像大小。
初始化边界参数
边界大小(top、bottom、left、right)被设为原图尺寸的 5%:
top = (int) (0.05*src.rows); bottom = top;
left = (int) (0.05*src.cols); right = left;
Python 中对应的是 int(0.05 * src.shape[0]) 与 int(0.05 * src.shape[1])(分别取行数与列数)。可见四个方向可以独立指定,本示例为了视觉效果令上下、左右各自对称。
主循环与按键处理
程序运行在无限循环中,直到用户按下 ESC 退出。每次循环先根据按键更新 borderType:
char c = (char)waitKey(500);
if( c == 27 )
{ break; }
else if( c == 'c' )
{ borderType = BORDER_CONSTANT; }
else if( c == 'r' )
{ borderType = BORDER_REPLICATE; }
waitKey(500) 会等待用户按键最多 500ms,返回值正是按键的 ASCII 码:27 对应 ESC,'c'(99)切换到恒定值模式,'r'(114)切换到复制模式。这也解释了“边框颜色每 0.5 秒随机刷新一次”——即使不按键,循环每 500ms 也会重绘一次。
随机颜色
每次循环迭代都会生成一个新的随机边框颜色:
Scalar value( rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255) );
value 是一个由三个在 [0, 255] 区间内随机抽取的整数组成的集合,分别对应 BGR 三通道,作为 BORDER_CONSTANT 模式的填充色。Python 版等价写法为 value = [randint(0, 255), randint(0, 255), randint(0, 255)]。
调用 copyMakeBorder 形成边框
最后调用核心函数:
copyMakeBorder( src, dst, top, bottom, left, right, borderType, value );
各参数依次为:
src:源图像;dst:目标图像;top、bottom、left、right:图像四侧的边界长度(像素),示例中为其原图尺寸的 5%;borderType:边界类型,本示例只取BORDER_CONSTANT或BORDER_REPLICATE;value:当borderType为BORDER_CONSTANT时用于填充边界像素的取值(Scalar三通道)。
显示结果
将填充后的 dst 显示到先前创建的窗口中,随后进入下一次循环:
imshow( window_name, dst );
运行结果
编译并运行程序,传入一张图片路径作为参数:
./copyMakeBorder_demo path/to/image
python copy_make_border.py path/to/image
预期表现:
- 程序默认以
BORDER_CONSTANT启动,因此会看到一系列随机彩色边框交替出现(每 0.5 秒换一种颜色); - 按下
r,边框变为边缘像素的复制; - 按下
c,随机彩色边框再次出现; - 按下
ESC,程序退出。
下图为实际运行截图,可清晰看到随机恒定色边框,以及 BORDER_REPLICATE 模式下边框像素与图像边缘像素连成一片、仿佛图像“外延”的效果:
源码深处:copyMakeBorder 是如何实现的
copyMakeBorder 的声明位于 core 模块(modules/core/include/opencv2/core.hpp),但其实际是作为“数组级基础操作”随 core 一起编译,实现在 modules/core/src/copy.cpp 中。阅读实现可以澄清几个容易被忽视的细节:
1. 前置断言:不允许负向“收缩”
函数入口处首先做参数校验:
CV_Assert( top >= 0 && bottom >= 0 && left >= 0 && right >= 0 && _src.dims() <= 2);
即各方向边界长度必须非负,且只支持二维图像;否则直接抛出异常。要“裁掉”边缘请改用 Rect/Range 取 ROI,而不是给 copyMakeBorder 传负值。
2. ROI 场景与 BORDER_ISOLATED 的真相
当 src 是大图像的一个子矩阵(ROI),且未指定 BORDER_ISOLATED 时,实现会先尝试“借用”母图像素:
if( src.isSubmatrix() && (borderType & BORDER_ISOLATED) == 0 )
{
src.locateROI(wholeSize, ofs);
int dtop = std::min(ofs.y, top);
int dbottom = std::min(wholeSize.height - src.rows - ofs.y, bottom);
int dleft = std::min(ofs.x, left);
int dright = std::min(wholeSize.width - src.cols - ofs.x, right);
src.adjustROI(dtop, dbottom, dleft, dright);
...
}
adjustROI 把 ROI 向外“撑大”,最多撑到母图边界;撑不满的部分再用后续的外推逻辑补齐。这正是头文件注释中“当源图像是更大图像的一部分(ROI)时,函数会尝试使用 ROI 外的像素来构成边界”的由来——若希望始终纯外推,请组合 borderType | BORDER_ISOLATED。
3. 分发策略:OpenCL / IPP / 标量内核
接下来函数按硬件能力与后端分发执行:
- 目标为 UMat 且编译期启用 OpenCL 时,进入
ocl_copyMakeBorder(modules/core/src/copy.cpp),它把边界模式映射为 OpenCL kernel 字符串"BORDER_CONSTANT"、"BORDER_REPLICATE"、"BORDER_REFLECT"、"BORDER_WRAP"、"BORDER_REFLECT_101",对应内核源码为 modules/core/src/opencl/copymakeborder.cl; - CPU 路径优先尝试 IPP 快速实现
ipp_copyMakeBorder(宏CV_IPP_RUN_FAST包裹); - 兜底路径则按模式二选一:
BORDER_CONSTANT之外的模式 →copyMakeBorder_8u(按行/列生成坐标映射表tab,逐像素外推);BORDER_CONSTANT→ 先把Scalar value通过scalarToRawData转换为原始像素字节,再调用copyMakeConstBorder_8u做整块填充。
4. 边界索引的数学基础:borderInterpolate
copyMakeBorder 的 doc 注释将更细的外推语义指向 borderInterpolate(p, len, borderType)(声明见 modules/core/include/opencv2/core.hpp):给定坐标 p(可能越界为负或 >= len)与数组长度 len,返回实际应读取的像素下标。其头文件注释给出过一个 BORDER_REFLECT_101 的实例:对 8 像素宽的行,外推结果为 [2 1 0 1 2 3 4 5 4 3](两侧各外扩 2),即反射但不重复边缘像素。需要指出的是,当 borderType == BORDER_CONSTANT 时该函数恒返回 -1,表示“不需要插值”,填充像素应直接取 value——这正是 copyMakeConstBorder_8u 独立成路的原因。
在仓库中的实际应用:谁在用 copyMakeBorder
copyMakeBorder 几乎是无处不在的“边界基础设施”。除本教程外,imgproc 内部大量算法都通过它预先扩边,以省去逐像素判断边界的开销,典型的调用点包括(均在 modules/imgproc/src 下):
- 中值滤波:
median_blur.simd.hpp中先对src扩出ksize/2的复制边界(BORDER_REPLICATE|BORDER_ISOLATED),使滑动窗口永不越界(对应 modules/imgproc/src/median_blur.simd.hpp); - 双边滤波:
bilateral_filter.dispatch.cpp多处用copyMakeBorder(src, temp, radius, radius, radius, radius, borderType)将图像预扩radius,见 modules/imgproc/src/bilateral_filter.dispatch.cpp; - 自适应直方图均衡(CLAHE):将图像补足到分块尺寸的整数倍(
BORDER_REFLECT_101),见 modules/imgproc/src/clahe.cpp; - 相位相关
phaseCorrelate:将两幅图像与窗函数零填充(BORDER_CONSTANT, Scalar::all(0))到相同尺寸后做 FFT,见 modules/imgproc/src/phasecorr.cpp; - 模板匹配、轮廓提取、去马赛克(demosaicing) 等也分别在 modules/imgproc/src/templmatch.cpp、modules/imgproc/src/contours_new.cpp、modules/imgproc/src/demosaicing.cpp 中以 1~2 像素小边界做预扩。
这些调用印证了教程“理论”一节的观点:大多数 OpenCV 滤波函数都会先扩边再卷积,而 copyMakeBorder 正是把这种“扩边”能力开放给你自己的自定义算法的最小单元。
后续进阶
若你希望自行编写基于卷积的边界处理,可先阅读 图像卷积(filter2D)教程 理解核与锚点;确认边界外推语义后,可进一步参考 Sobel 导数教程(sobel_derivatives),其中滤波函数的 borderType 参数默认值即来自本文介绍的 BorderTypes 家族。官方同时将本文配套示例注册于 copyMakeBorder 在 core.hpp 中的 example 入口,读者可在仓库任意路径用 opencv_version 构建环境或直接编译 samples 目录下的演示代码进行验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
