首页
/ OpenCV copyMakeBorder 图像边界填充教程:原理、三种语言实现与底层源码解析

OpenCV copyMakeBorder 图像边界填充教程:原理、三种语言实现与底层源码解析

2026-09-06 19:01:50作者:韦蓉瑛

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 家族

copyMakeBordermodules/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++ 完整代码

/**
 * @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++ 的窗口体系,按键统一转为小写后比较,因此 Cc 均可切换恒定值模式。

程序逻辑逐段拆解

声明变量

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 表示窗口尺寸自动适配图像大小。

初始化边界参数

边界大小(topbottomleftright)被设为原图尺寸的 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 );

各参数依次为:

  1. src:源图像;
  2. dst:目标图像;
  3. topbottomleftright:图像四侧的边界长度(像素),示例中为其原图尺寸的 5%;
  4. borderType:边界类型,本示例只取 BORDER_CONSTANTBORDER_REPLICATE
  5. value:当 borderTypeBORDER_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 演示程序运行结果:左为 BORDER_REPLICATE 复制边缘效果,右为随机颜色 BORDER_CONSTANT 填充效果

源码深处: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_copyMakeBordermodules/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 下):

这些调用印证了教程“理论”一节的观点:大多数 OpenCV 滤波函数都会先扩边再卷积,而 copyMakeBorder 正是把这种“扩边”能力开放给你自己的自定义算法的最小单元。

后续进阶

若你希望自行编写基于卷积的边界处理,可先阅读 图像卷积(filter2D)教程 理解核与锚点;确认边界外推语义后,可进一步参考 Sobel 导数教程(sobel_derivatives),其中滤波函数的 borderType 参数默认值即来自本文介绍的 BorderTypes 家族。官方同时将本文配套示例注册于 copyMakeBorder 在 core.hpp 中的 example 入口,读者可在仓库任意路径用 opencv_version 构建环境或直接编译 samples 目录下的演示代码进行验证。

登录后查看全文
热门项目推荐
相关项目推荐