首页
/ OpenCV 基础绘图实战:用 line、ellipse、rectangle、circle 与 fillPoly 绘制原子和城堡图形

OpenCV 基础绘图实战:用 line、ellipse、rectangle、circle 与 fillPoly 绘制原子和城堡图形

2026-09-06 18:43:11作者:滕妙奇

本篇技术指南围绕 OpenCV 官方《Basic Drawing》基础绘图教程展开,讲解如何用 line()ellipse()rectangle()circle()fillPoly() 在纯黑色画布上绘制"原子"与"城堡(国际象棋车)"两组图形。通过本教程,你将掌握 OpenCV 中最常用的几何图元绘制 API 的参数语义(坐标、BGR 颜色、线宽与线型、填充模式),并能在 C++、Python 与 Java 三种语言之间互译这些绘制调用,为后续的随机图形生成、文字叠加以及各类视觉标注功能打下基础。本教程由 Ana Huamán 编写,适用于 OpenCV 3.0 及更高版本,教程原文位于 basic_geometric_drawing.markdown

本教程目标

阅读并运行示例之后,你将学会使用以下五个核心绘图函数:

  • 使用 line() 绘制一条线段;
  • 使用 ellipse() 绘制椭圆或椭圆弧;
  • 使用 rectangle() 绘制矩形轮廓或填充矩形;
  • 使用 circle() 绘制圆;
  • 使用 fillPoly() 绘制填充多边形。

为了把上面五个函数串起来,教程安排了"一个原子 + 一座城堡"两个绘制任务:原子由四条不同旋转角度的椭圆弧线与一个红色实心圆构成;城堡由白色填充的多边形、黄色填充矩形底座和若干黑色分隔线构成。

理论基础:Point 与 Scalar

在整个教程中,几乎所有绘图函数都需要传入两类基本结构:点的坐标与颜色值。C++/Java 中对应 cv::Pointcv::Scalar,而 Python 中则直接使用元组表示。

cv::Point:二维图像坐标点

cv::Point 表示一个二维点,由图像坐标系下的 xy 指定。C++ 中可以通过成员赋值或构造函数创建:

Point pt;
pt.x = 10;
pt.y = 8;

或等价地:

Point pt = Point(10, 8);

Java 版本与之类似:

Point pt = new Point();
pt.x = 10;
pt.y = 8;
// 或者
Point pt = new Point(10, 8);

Python 版本直接使用包含两个元素的元组代表点坐标:

pt = (10, 0)  # x = 10, y = 0

需要说明的是,OpenCV 中图像坐标系的 x 轴向右为正、y 轴向下为正,且单位是像素。cv::Pointcv::Scalar 等基础类型定义在核心模块头文件 types.hpp 中。

cv::Scalar:BGR 颜色载体

  • Scalar 是一个 4 元素向量,OpenCV 中广泛用它来传递像素值(对应头文件中的 Scalar_ 类型)。
  • 本教程用它表示 BGR(蓝-绿-红)颜色,通常只使用前三个参数,第四个参数(Alpha)不使用时就无需给出。
  • 例如,当你向函数传入:
Scalar(a, b, c)

对应的 BGR 颜色即为:Blue = a,Green = b,Red = c

Java 中同样使用 new Scalar(a, b, c);Python 中则使用普通元组 (a, b, c)

特别注意颜色通道顺序:OpenCV 的颜色约定是 BGR 而非常见的 RGB。头文件 imgproc.hpp 中用宏 CV_RGB(r, g, b) 直接等价于 cv::Scalar(b, g, r, 0) 佐证了这一通道顺序。因此,本教程中的常见颜色写为:

颜色 BGR 值(Scalar / 元组) 用途示例
蓝 Blue Scalar(255, 0, 0) 原子的椭圆
红 Red Scalar(0, 0, 255) 原子中心的实心圆
白 White Scalar(255, 255, 255) 城堡的主体多边形
黄 Yellow Scalar(0, 255, 255) 城堡的底座矩形
黑 Black Scalar(0, 0, 0) 底座上的分隔线

示例代码位置:三种语言的完整入口

本教程对应的完整示例程序分布在仓库的 samples 目录中(原文档以 @include 形式内嵌了这三份代码):

  • C++:Drawing_1.cpp,包含 4 个封装函数 MyEllipseMyFilledCircleMyPolygonMyLine
  • Java:BasicGeometricDrawing.java,核心逻辑放在 GeometricDrawingRun 类中,由 BasicGeometricDrawing.main 加载原生库后调用;
  • Python:basic_geometric_drawing.py,同名的小写函数风格(my_ellipsemy_filled_circle 等),坐标一律使用整数除法 W // 2

由于这些函数会直接写入调用方传入的图像对象,因此三种语言中封装函数的第一个参数都是图像本身。下面以 C++ 为例给出完整主程序,让你先看到全局流程:

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

#define w 400

using namespace cv;

/// Function headers
void MyEllipse( Mat img, double angle );
void MyFilledCircle( Mat img, Point center );
void MyPolygon( Mat img );
void MyLine( Mat img, Point start, Point end );

int main( void ){

  /// Windows names
  char atom_window[] = "Drawing 1: Atom";
  char rook_window[] = "Drawing 2: Rook";

  /// Create black empty images
  Mat atom_image = Mat::zeros( w, w, CV_8UC3 );
  Mat rook_image = Mat::zeros( w, w, CV_8UC3 );

  /// 1. Draw a simple atom:
  /// 1.a. Creating ellipses
  MyEllipse( atom_image, 90 );
  MyEllipse( atom_image, 0 );
  MyEllipse( atom_image, 45 );
  MyEllipse( atom_image, -45 );

  /// 1.b. Creating circles
  MyFilledCircle( atom_image, Point( w/2, w/2) );

  /// 2. Draw a rook
  /// 2.a. Create a convex polygon
  MyPolygon( rook_image );

  /// 2.b. Creating rectangles
  rectangle( rook_image,
         Point( 0, 7*w/8 ),
         Point( w, w),
         Scalar( 0, 255, 255 ),
         FILLED,
         LINE_8 );

  /// 2.c. Create a few lines
  MyLine( rook_image, Point( 0, 15*w/16 ), Point( w, 15*w/16 ) );
  MyLine( rook_image, Point( w/4, 7*w/8 ), Point( w/4, w ) );
  MyLine( rook_image, Point( w/2, 7*w/8 ), Point( w/2, w ) );
  MyLine( rook_image, Point( 3*w/4, 7*w/8 ), Point( 3*w/4, w ) );

  /// 3. Display your stuff!
  imshow( atom_window, atom_image );
  moveWindow( atom_window, 0, 200 );
  imshow( rook_window, rook_image );
  moveWindow( rook_window, w, 200 );

  waitKey( 0 );
  return(0);
}

Python 版主流程与 C++ 几乎一一对应,区别在于使用 numpy 创建零矩阵模拟黑色画布、用 cv2 前缀调用函数,并显式调用 destroyAllWindows() 收尾,完整脚本见 basic_geometric_drawing.py。Java 版还需在 main 中先执行 System.loadLibrary(Core.NATIVE_LIBRARY_NAME) 加载 OpenCV 原生库,窗口函数改用 HighGui.imshow/moveWindow/waitKey,见 BasicGeometricDrawing.java

代码解剖:从主流程理解两个绘制任务

主流程一共分三步:

  1. 创建两张纯黑图像:C++ 用 Mat::zeros(w, w, CV_8UC3),表示宽、高均为 w = 400、每像素 3 通道(BGR)的 8 位无符号图,初始值全 0(黑色)。Python 用 np.zeros((w, w, 3), dtype=np.uint8),Java 用 Mat.zeros(W, W, CvType.CV_8UC3),三者的语义完全一致。
  2. 分别调用封装函数绘制"原子"和"城堡":原子调用 MyEllipse(4 次,旋转角为 90、0、45、-45 度)与 MyFilledCircle;城堡调用 MyPolygonrectangle 与 4 次 MyLine
  3. 把结果显示到屏幕上imshow 显示图像、moveWindow 把窗口移动到指定屏幕坐标(原子窗口在 (0, 200),城堡窗口在 (w, 200),使两个窗口并排显示),最后 waitKey(0) 无限期等待用户按键后退出程序。

下面逐个拆解每个封装函数内部的绘图调用,并同步给出 Python/Java 的等价写法与 OpenCV 源码层面的语义。

MyLine:cv::line() 画线段

void MyLine( Mat img, Point start, Point end )
{
  int thickness = 2;
  int lineType = LINE_8;

  line( img, start, end, Scalar( 0, 0, 0 ), thickness, lineType );
}

对应 Python:

def my_line(img, start, end):
    thickness = 2
    line_type = 8
    cv.line(img, start, end, (0, 0, 0), thickness, line_type)

根据官方 API 文档 imgproc.hpp#L3873-L3889line() 的作用是:

  • 在图像 img 上,从起点 start 到终点 end 画一条线段,线段会被图像边界自动裁剪;
  • 颜色为 Scalar(0, 0, 0),即 BGR 下的黑色
  • 线宽 thickness = 2(像素);
  • 线型 lineType = 8,即 8-连通线段(8-connected Bresenham 算法);若为 4 则使用 4-连通 Bresenham,若为抗锯齿线型则内部采用高斯滤波实现平滑。

在城堡示例中,MyLine 被调用 4 次:一条水平线画在 y = 15*w/16 处作为底座与上部的分界,另三条垂直线分别位于 x = w/4、w/2、3*w/4,从 7*w/8 画到 w,把底座视觉上分隔成三个门洞。

MyEllipse:cv::ellipse() 画椭圆

void MyEllipse( Mat img, double angle )
{
  int thickness = 2;
  int lineType = 8;

  ellipse( img,
       Point( w/2, w/2 ),   // 中心点
       Size( w/4, w/16 ),   // 轴长的一半(半轴)
       angle,               // 旋转角度(度)
       0,                   // 起始角度
       360,                 // 结束角度
       Scalar( 255, 0, 0 ), // BGR 蓝色
       thickness,
       lineType );
}

对应 Python:

def my_ellipse(img, angle):
    thickness = 2
    line_type = 8
    cv.ellipse(img,
               (W // 2, W // 2),
               (W // 4, W // 16),
               angle,
               0,
               360,
               (255, 0, 0),
               thickness,
               line_type)

imgproc.hpp#L3974-L4002 的说明可以看出 ellipse() 的关键语义:

  • 椭圆绘制在 img 上,中心位于 (w/2, w/2)
  • Size(w/4, w/16) 表示主轴半径的一半,即该椭圆被一个 (w/4, w/16) 的外接盒所包含——扁长的形状加上 4 种旋转角(90°、0°、45°、-45°),就形成了原子外围的"电子轨道"效果;
  • angle 是椭圆自身的旋转角度,单位是度;
  • startAngle = 0endAngle = 360 表示绘制完整的椭圆(而不是弧线)。若要画弧线只需修改这两个角度;若 startAngle 大于 endAngle,OpenCV 会自动交换二者;
  • 颜色 Scalar(255, 0, 0) 是 BGR 下的蓝色
  • thickness = 2 为轮廓线宽;当 thickness 为负值(如 FILLED)时,将填充整个椭圆(或填充对应的椭圆扇形)。

需要补充的底层细节是:ellipse 的弧线边界用分段的折线(piecewise-linear curve)近似,需要更精细控制时可用 ellipse2Poly() 先取轮廓点,再用 polylines() 描边或 fillPoly() 填充(同一文档注释已明确这一替代路径)。

MyFilledCircle:cv::circle() 画实心圆

void MyFilledCircle( Mat img, Point center )
{
  circle( img,
      center,             // 圆心 (w/2, w/2)
      w/32,               // 半径
      Scalar( 0, 0, 255 ),// BGR 红色
      FILLED,             // thickness = -1,填充
      LINE_8 );
}

对应 Python:

def my_filled_circle(img, center):
    thickness = -1
    line_type = 8
    cv.circle(img, center, W // 32, (0, 0, 255), thickness, line_type)

circle() 的语义(见 imgproc.hpp#L3958-L3972):

  • img 上、以点 center 为圆心绘制圆;
  • 半径为 w/32(约 12 像素),这里位于画面正中心的 (w/2, w/2)
  • 颜色 Scalar(0, 0, 255) 是 BGR 下的红色
  • 由于 thickness = -1(即枚举 FILLED),圆将被实心填充。头文件中明确写着:thickness 为正数时画轮廓,负值(如 FILLED)表示画填充圆。

这样,4 条蓝色扁椭圆轨线 + 1 个红色实心原子核,共同构成"原子"图案。

rectangle():画黄色底座

教程没有为矩形单独封装函数,而是直接在 main 中调用 cv::rectangle

rectangle( rook_image,
       Point( 0, 7*w/8 ),
       Point( w, w ),
       Scalar( 0, 255, 255 ),
       FILLED,
       LINE_8 );

对应 Python:

cv.rectangle(rook_image, (0, 7 * W // 8), (W, W), (0, 255, 255), -1, 8)

对应 Java(注意 Imgproc.rectangle 与额外 shift 参数):

Imgproc.rectangle( rook_image,
        new Point( 0, 7*W/8 ),
        new Point( W, W),
        new Scalar( 0, 255, 255 ),
        -1,   // thickness
        8,    // lineType
        0 );  // shift

imgproc.hpp#L3927-L3952 可以确认 rectangle() 的两种重载形式,本教程使用的是"两个对角顶点"形式:

  • 矩形绘制在 rook_image 上,两个相对顶点为 Point(0, 7*w/8)Point(w, w),即占据画布最底部约 1/8 高度的横条;
  • 颜色 Scalar(0, 255, 255) 是 BGR 下的黄色
  • thickness = FILLED(数值为 -1),因此矩形被完全填充。

另一重载 rectangle(img, Rect rec, ...) 接受 cv::Rect,其内部等价于用 rec.tl()rec.br() - Point(1, 1) 作为对角顶点。

MyPolygon:cv::fillPoly() 画填充多边形(城堡主体)

void MyPolygon( Mat img )
{
  int lineType = LINE_8;

  /** Create some points */
  Point rook_points[1][20];
  rook_points[0][0]  = Point(    w/4,   7*w/8 );
  rook_points[0][1]  = Point(  3*w/4,   7*w/8 );
  /* ... 共 20 个顶点,按逆时针勾勒出城堡轮廓 ... */
  rook_points[0][18] = Point(  5*w/16, 13*w/16 );
  rook_points[0][19] = Point(    w/4,  13*w/16 );

  const Point* ppt[1] = { rook_points[0] };
  int npt[] = { 20 };

  fillPoly( img, ppt, npt, 1,
        Scalar( 255, 255, 255 ),  // BGR 白色
        lineType );
}

对应 Python,顶点用 numpy 数组组织,并通过 reshape((-1, 1, 2)) 转成"顶点数 × 1 × 2"的形状交给 cv.fillPoly

def my_polygon(img):
    line_type = 8
    # Create some points
    ppt = np.array([[W / 4, 7 * W / 8], [3 * W / 4, 7 * W / 8],
                    [3 * W / 4, 13 * W / 16], [11 * W / 16, 13 * W / 16],
                    [19 * W / 32, 3 * W / 8], [3 * W / 4, 3 * W / 8],
                    [3 * W / 4, W / 8], [26 * W / 40, W / 8],
                    [26 * W / 40, W / 4], [22 * W / 40, W / 4],
                    [22 * W / 40, W / 8], [18 * W / 40, W / 8],
                    [18 * W / 40, W / 4], [14 * W / 40, W / 4],
                    [14 * W / 40, W / 8], [W / 4, W / 8],
                    [W / 4, 3 * W / 8], [13 * W / 32, 3 * W / 8],
                    [5 * W / 16, 13 * W / 16], [W / 4, 13 * W / 16]], np.int32)
    ppt = ppt.reshape((-1, 1, 2))
    cv.fillPoly(img, [ppt], (255, 255, 255), line_type)
    # Only drawing the lines would be:
    # cv.polylines(img, [ppt], True, (255, 0, 255), line_type)

Java 版则将 20 个 Point 放入 MatOfPoint,再包成 List<MatOfPoint> 后传给 Imgproc.fillPoly(源码见 BasicGeometricDrawing.java)。

fillPoly() 的关键事实(见 imgproc.hpp#L4068-L4089):

  • 多边形绘制在 img 上;
  • 顶点集由 ppt 提供——C 接口以 const Point** ptsconst int* npts 和轮廓数 ncontours 描述(本示例为 1 条轮廓、20 个顶点),C++ 包装接口 InputArrayOfArrays pts 则接受一组多边形顶点数组;
  • 颜色 Scalar(255, 255, 255) 是 BGR 下的白色
  • 该函数可以填充"由一条或多条多边形轮廓围成的区域",支持带孔洞的复杂区域甚至部分自相交的轮廓。

这 20 个顶点勾勒出的白色区域形如城堡上方的城垛:两侧有凸起、中间有几处内凹(注意代码注释称之为 concave polygon),顶点坐标全部以 w 的分数形式给出,因此把 w 换成任意尺寸都能等比缩放。Python 代码注释还提示:若只想画轮廓而不填充,可改用 cv.polylines(img, [ppt], True, ...)isClosed = True 表示首尾自动闭合)。

参数速查:线宽、线型与填充

在上面的调用中反复出现了 thicknesslineTypeFILLED,它们统一受 imgproc.hpp#L875-L879LineTypes 枚举约束:

枚举值 数值 含义
FILLED -1 负线宽即"填充模式":圆、椭圆、矩形、多边形一律绘制成实心
LINE_4 4 4-连通线段
LINE_8 8 8-连通线段(本教程默认,速度最快)
LINE_AA 16 抗锯齿线段,边缘更平滑但更耗时

各绘图函数在 imgproc.hpp 中的默认参数均为 thickness = 1, lineType = LINE_8, shift = 0shift 表示坐标/半径中保留的小数位数(通常为 0)。当需要绘制后续章节会提到的箭头、文字、轮廓标注等内容时,arrowedLineputTextdrawContoursdrawMarkerpolylines 等函数与这五个基础函数共用同一套线宽/线型约定(后两者同样声明在 imgproc.hpp#L4033-L4035imgproc.hpp#L4104-L4106)。

底层实现:这些绘图函数在 imgproc 模块中的位置

本教程的绘制 API 声明全部集中在 imgproc.hppimgproc_draw 分组(约第 3866 行开始),而实现位于 imgproc 模块的绘图引擎 drawing.cpp,各函数定义起始行分别为:

需要指出,官方还提供了与 fillPoly 功能相近但更快的 fillConvexPoly()(见 imgproc.hpp#L4041-L4061),它只能填充凸多边形或"与每条扫描线最多相交两次"的单调多边形;本教程的城堡是凹多边形,因此必须使用通用性更强的 fillPoly。绘图底层使用整数 Bresenham/扫描线类算法直接在图像缓冲区上写入像素,绘制结果与图层无关——先画的内容会被后画的内容覆盖,所以本教程刻意把顺序安排为:先 fillPoly 填充主体、再 rectangle 填充底座、最后画黑色分隔线,后画的黑色线自然落在黄色底座之上。

运行示例并查看结果

编译并运行示例后,应能得到下图所示的效果(左侧为原子、右侧为城堡,两图均已在窗口中并排展示):

OpenCV 基础绘图教程运行结果:左侧蓝色椭圆轨线与红色实心圆构成原子,右侧白色多边形、黄色矩形底座与黑色分隔线构成城堡

针对不同语言的运行方式:

  • C++:示例已纳入 OpenCV 源码树(samples/cpp/tutorial_code/ImgProc/basic_drawing/Drawing_1.cpp)。常规做法是先用 CMake 配置并构建带示例的 OpenCV(启用 BUILD_EXAMPLES 后样本会被编译为可执行文件);也可以在有 opencv4.pc 或 OpenCVConfig 的环境下手动编译,例如 g++ Drawing_1.cpp -o drawing_1 $(pkg-config --cflags --libs opencv4),具体链接选项取决于本机 OpenCV 的安装方式。
  • Python:安装 opencv-python(或本仓库编译产出的 Python 绑定)与 numpy 后,直接运行 python samples/python/tutorial_code/imgProc/BasicGeometricDrawing/basic_geometric_drawing.py。脚本通过 cv.waitKey(0) 等待按键,随后 cv.destroyAllWindows() 释放窗口。
  • Java:需要先构建 OpenCV 的 Java 绑定(得到 opencv-*.jar 与原生库),运行时在 classpath 中包含 jar 并把原生库所在目录加入 java.library.path。示例代码中的 System.loadLibrary(Core.NATIVE_LIBRARY_NAME) 会负责加载原生库,窗口交互通过 HighGui 完成。

由于 waitKey(0) 会一直阻塞到任意按键事件,程序将停留在两张绘图窗口上等待你查看;按下任意键后,程序正常退出。

继续深入学习

本教程是 OpenCV 绘图主题系列的第一篇。官方把绘制完成后"在图上叠加随机颜色与文字"的进阶内容放在了下一篇教程中,即 random_generator_and_text.markdown,那里将展示 RNG(随机数发生器)、putTextgetTextSize 的用法,并借助本教程学到的绘图函数生成动态画面。如果你希望系统梳理 imgproc 模块的全部主题,可参阅 table_of_content_imgproc.markdown;而 cv::linecv::circlecv::ellipse 等函数的精确参数注释与默认值,则随时可以回到权威头文件 imgproc.hpp 中查阅。

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