OpenCV 基础绘图实战:用 line、ellipse、rectangle、circle 与 fillPoly 绘制原子和城堡图形
本篇技术指南围绕 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::Point 和 cv::Scalar,而 Python 中则直接使用元组表示。
cv::Point:二维图像坐标点
cv::Point 表示一个二维点,由图像坐标系下的 x 与 y 指定。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::Point、cv::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 个封装函数
MyEllipse、MyFilledCircle、MyPolygon、MyLine; - Java:BasicGeometricDrawing.java,核心逻辑放在
GeometricDrawingRun类中,由BasicGeometricDrawing.main加载原生库后调用; - Python:basic_geometric_drawing.py,同名的小写函数风格(
my_ellipse、my_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。
代码解剖:从主流程理解两个绘制任务
主流程一共分三步:
- 创建两张纯黑图像: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),三者的语义完全一致。 - 分别调用封装函数绘制"原子"和"城堡":原子调用
MyEllipse(4 次,旋转角为 90、0、45、-45 度)与MyFilledCircle;城堡调用MyPolygon、rectangle与 4 次MyLine。 - 把结果显示到屏幕上:
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-L3889,line() 的作用是:
- 在图像
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 = 0、endAngle = 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** pts、const 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 表示首尾自动闭合)。
参数速查:线宽、线型与填充
在上面的调用中反复出现了 thickness、lineType 与 FILLED,它们统一受 imgproc.hpp#L875-L879 中 LineTypes 枚举约束:
| 枚举值 | 数值 | 含义 |
|---|---|---|
FILLED |
-1 |
负线宽即"填充模式":圆、椭圆、矩形、多边形一律绘制成实心 |
LINE_4 |
4 |
4-连通线段 |
LINE_8 |
8 |
8-连通线段(本教程默认,速度最快) |
LINE_AA |
16 |
抗锯齿线段,边缘更平滑但更耗时 |
各绘图函数在 imgproc.hpp 中的默认参数均为 thickness = 1, lineType = LINE_8, shift = 0;shift 表示坐标/半径中保留的小数位数(通常为 0)。当需要绘制后续章节会提到的箭头、文字、轮廓标注等内容时,arrowedLine、putText、drawContours、drawMarker、polylines 等函数与这五个基础函数共用同一套线宽/线型约定(后两者同样声明在 imgproc.hpp#L4033-L4035 与 imgproc.hpp#L4104-L4106)。
底层实现:这些绘图函数在 imgproc 模块中的位置
本教程的绘制 API 声明全部集中在 imgproc.hpp 的 imgproc_draw 分组(约第 3866 行开始),而实现位于 imgproc 模块的绘图引擎 drawing.cpp,各函数定义起始行分别为:
line():drawing.cpp#L1827;rectangle()(两点形式 / Rect 形式):drawing.cpp#L1866、drawing.cpp#L1899;circle():drawing.cpp#L1917;ellipse()(中心+轴形式 / RotatedRect 形式):drawing.cpp#L1948、drawing.cpp#L1979;fillConvexPoly():drawing.cpp#L2007;fillPoly():drawing.cpp#L2027;polylines():drawing.cpp#L2065。
需要指出,官方还提供了与 fillPoly 功能相近但更快的 fillConvexPoly()(见 imgproc.hpp#L4041-L4061),它只能填充凸多边形或"与每条扫描线最多相交两次"的单调多边形;本教程的城堡是凹多边形,因此必须使用通用性更强的 fillPoly。绘图底层使用整数 Bresenham/扫描线类算法直接在图像缓冲区上写入像素,绘制结果与图层无关——先画的内容会被后画的内容覆盖,所以本教程刻意把顺序安排为:先 fillPoly 填充主体、再 rectangle 填充底座、最后画黑色分隔线,后画的黑色线自然落在黄色底座之上。
运行示例并查看结果
编译并运行示例后,应能得到下图所示的效果(左侧为原子、右侧为城堡,两图均已在窗口中并排展示):
针对不同语言的运行方式:
- 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(随机数发生器)、putText 与 getTextSize 的用法,并借助本教程学到的绘图函数生成动态画面。如果你希望系统梳理 imgproc 模块的全部主题,可参阅 table_of_content_imgproc.markdown;而 cv::line、cv::circle、cv::ellipse 等函数的精确参数注释与默认值,则随时可以回到权威头文件 imgproc.hpp 中查阅。
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 StartedRust0625
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
