OpenCV 基础数据类型与泛型操作:从"原子"到"工具箱"的完整图谱
本文参考 OpenCV 官方文档 及 OpenCV 4.x 教程 撰写。
1. 为什么我们要从"原子"开始?
别急,我知道你可能已经迫不及待想直接上手 cv::Mat 做图像处理了。但想想看——如果不知道构成图像的"原子"是什么,我们又怎么理解图像背后的数学呢?
OpenCV 的 core 模块就像一张精密的乐高图纸。cv::Mat 是那座宏伟的城堡,而 Point_、Size_、Rect_、Scalar_、Matx、Vec……这些基础类型,就是那些最小的积木块。它们虽然不起眼,但每一个都承载着特定的几何语义和运算规则。
在这篇文章里,我们将一起探索 OpenCV 的基础数据类型体系,从最基础的几何点开始,层层深入到小型矩阵、标量颜色,以及辅助工具类。每节只解决一个认知疑点,用对话式的语气,像朋友聊天一样带你探索。
2. 整体架构:OpenCV 基础类型的"家族树"
在继续之前,让我们先看看 OpenCV 基础类型的整体架构。如果画成图,会是什么样子呢?
现在我们已经了解了整体架构,接下来让我们从最小的积木块——几何点开始探索。
3. 几何点与向量类型
3.1 Point_/Point2f/Point3f 的坐标运算重载与范数计算
听起来抽象对吧?一个"点"能有多复杂?但在计算机视觉里,一个像素坐标 (x, y) 不仅仅是一个位置,它还是一个向量——可以从原点出发,有方向、有长度。
OpenCV 用模板类 Point_<T> 来表示这种几何点。Point2i(整数坐标)、Point2f(浮点坐标)、Point3f(三维坐标)都是它的具体化身。
第一步:创建点
#include <opencv2/opencv.hpp>
using namespace cv;
// 创建二维浮点坐标点
Point2f p1(3.0f, 4.0f); // 就像平面上的一个位置
Point2f p2(1.0f, 2.0f);
// 三维点
Point3f p3d(1.0f, 2.0f, 2.0f);
第二步:算术运算重载
OpenCV 为 Point_ 重载了全套算术运算符,让它们可以像数字一样直接加减乘除:
// 向量加减:就像力的合成
Point2f p_add = p1 + p2; // (4.0, 6.0) —— 两个向量的"合力"
Point2f p_sub = p1 - p2; // (2.0, 2.0) —— 从p2指向p1的向量
// 标量乘法:缩放向量长度
Point2f p_mul = p1 * 2.0f; // (6.0, 8.0) —— 长度翻倍,方向不变
Point2f p_div = p1 / 2.0f; // (1.5, 2.0) —— 长度减半
第三步:范数计算——向量的"长度"
如果画成图,一个从原点出发到 (3, 4) 的向量,它的长度是多少?根据勾股定理,是 sqrt(3² + 4²) = 5。这就是 L2 范数。
// L2范数(欧几里得距离)—— 最常用的"长度"
float norm_l2 = norm(p1); // 5.0
// L1范数(曼哈顿距离)—— 各分量绝对值之和
float norm_l1 = norm(p1, NORM_L1); // |3| + |4| = 7.0
// L∞范数(切比雪夫距离)—— 最大分量
float norm_inf = norm(p1, NORM_INF); // max(|3|, |4|) = 4.0
在继续之前,让我们看看这些范数在实际使用中意味着什么:
- L2 范数:计算两点之间的直线距离,用于最近邻搜索
- L1 范数:对异常值不敏感,用于鲁棒估计
- L∞ 范数:判断是否在正方形区域内,用于快速边界检测
3.2 点积/叉积与向量夹角的几何意义实现
现在我们已经了解了点的坐标运算,接下来看看更高级的操作——点积和叉积。
点积(Dot Product)—— 衡量"方向一致性"
两个向量的点积,几何意义是:一个向量在另一个向量方向上的投影长度,乘以另一个向量的长度。
如果画成图,就像一束光垂直照射在一个斜面上,投影的长度就是点积的一部分。
Point3f a(1.0f, 0.0f, 0.0f); // x轴单位向量
Point3f b(0.0f, 1.0f, 0.0f); // y轴单位向量
Point3f c(1.0f, 1.0f, 0.0f); // 45度方向
// 点积
float dot_ab = a.dot(b); // 0.0 —— 垂直向量点积为0!
float dot_ac = a.dot(c); // 1.0 —— 有夹角,点积为正
叉积(Cross Product)—— 生成"垂直向量"
叉积只在三维空间中定义,结果是同时垂直于两个输入向量的新向量。方向遵循右手定则。
// 叉积 —— 仅3D点支持
Point3f cross_ab = a.cross(b); // (0, 0, 1) —— z轴方向
向量夹角计算——从点积到角度
// 夹角公式: cos(θ) = (a·b) / (|a|*|b|)
float angle_ab = acos(a.dot(b) / (norm(a) * norm(b))) * 180.0 / CV_PI; // 90°
float angle_ac = acos(a.dot(c) / (norm(a) * norm(c))) * 180.0 / CV_PI; // 45°
这在训练/实际使用中意味着什么?在特征匹配中,我们常用点积来判断两个特征向量的相似度——点积越大,方向越接近,特征越相似。
3.3 点到线段距离、点到直线投影的公式推导
别急,这个听起来复杂,但如果我们把它拆解成几步,其实很直观。
问题:给定一个点 P 和一条线段 AB,怎么找到 P 到 AB 的最短距离?
如果画成图,最短距离一定是 P 到 AB 的垂线。但这里有个陷阱——垂足可能落在线段之外!
公式推导:
- 计算向量
AP = P - A,AB = B - A - 投影参数
t = (AP · AB) / |AB|²—— 表示 P 在 AB 方向上的"进度" - 如果
t < 0,最近点是 A;如果t > 1,最近点是 B;否则是投影点A + AB * t
Point2f p(5.0f, 5.0f);
Point2f line_start(0.0f, 0.0f);
Point2f line_end(10.0f, 0.0f); // x轴上的线段
// 第一步:计算向量
Point2f AP = p - line_start;
Point2f AB = line_end - line_start;
// 第二步:计算投影参数 t
float t = AP.dot(AB) / AB.dot(AB); // 投影参数 [0, 1]
// 第三步:判断最近点位置
Point2f closest;
if (t < 0) closest = line_start; // 垂足在A左侧
else if (t > 1) closest = line_end; // 垂足在B右侧
else closest = line_start + AB * t; // 垂足在线段内
// 第四步:计算距离
float dist = norm(p - closest);
这在实际使用中意味着什么?在碰撞检测、最近邻搜索、路径规划中,点到线段的距离是最基础的运算之一。
4. 区域描述类型
4.1 Size_ 的宽高语义与面积/宽高比计算
现在我们已经了解了点的运算,接下来看看如何描述一个区域的大小。
Size_<T> 就是专门干这个的——它只有两个成员:width 和 height。听起来简单,但这里有个微妙的语义问题:width 和 height 到底代表什么?
在 OpenCV 中,width 是水平方向的跨度,height 是垂直方向的跨度。这和数学中的坐标系是一致的。
Size2f sz(800.0f, 600.0f); // width=800, height=600
// 面积计算
float area = sz.area(); // 800 * 600 = 480000
// 宽高比(Aspect Ratio)—— 视频压缩、图像缩放的核心参数
float aspect_ratio = (float)sz.width / sz.height; // 4:3 = 1.333...
// 缩放操作
Size2f sz_scaled = sz * 0.5f; // (400, 300)
Size2f sz_div = sz / 2.0f; // (400, 300)
Size 与 Point 的关系:Size 可以看作是从原点出发的向量,终点坐标就是 (width, height)。所以 Size 和 Point 之间可以相互转换。
4.2 Rect_ 的 tl()/br()/交集/并集/包含判定运算
如果 Point 是位置,Size 是大小,那 Rect 就是位置和大小的结合——一个矩形区域。
Rect r1(100, 100, 200, 150); // x=100, y=100, width=200, height=150
交集与并集——两个矩形的"重叠"与"合并"
Rect r1(100, 100, 200, 150);
Rect r2(150, 120, 200, 200);
// 交集(&)—— 两个矩形重叠的区域
Rect r_inter = r1 & r2;
// 结果: x=150, y=120, width=150, height=130
// 并集(|)—— 能包围两个矩形的最小矩形
Rect r_union = r1 | r2;
// 结果: x=100, y=100, width=250, height=220
包含判定——点在不在矩形内?
Point pt_inside(200, 200);
Point pt_outside(50, 50);
bool contains_inside = r1.contains(pt_inside); // true
bool contains_outside = r1.contains(pt_outside); // false
矩形平移与缩放
// 平移:加上一个 Point
Rect r_moved = r1 + Point(50, 30); // 向右50, 向下30
// 缩放:乘以一个标量(注意:只缩放宽高,不缩放位置)
Rect r_scaled = r1 * 2; // width=400, height=300, x,y 不变
这在实际使用中意味着什么?在目标检测中,Rect 是边界框(Bounding Box)的标准表示;在图像裁剪中,Rect 定义了 ROI(感兴趣区域)。
4.3 RotatedRect 的最小外接矩形与四个顶点提取(boxPoints)
听起来抽象对吧?普通的 Rect 只能表示"正放"的矩形,但现实中的物体往往是倾斜的。怎么办?
RotatedRect 就是为了解决这个问题而生的。它有三个参数:
center:旋转中心size:宽高(注意:这里的 size 是旋转前的原始宽高)angle:旋转角度(顺时针,单位度)
// 构造旋转矩形
Point2f center(400.0f, 300.0f);
Size2f size(200.0f, 100.0f);
float angle = 30.0f; // 顺时针旋转30度
RotatedRect rRect(center, size, angle);
// 提取四个顶点
Point2f vertices[4];
rRect.points(vertices);
// 或者用 boxPoints 函数
Mat boxPts;
boxPoints(rRect, boxPts); // 输出 4x2 矩阵
最小外接矩形(minAreaRect)
给定一组轮廓点,找到能包围它们的最小面积旋转矩形:
vector<Point> contour;
// ... 填充轮廓点 ...
RotatedRect minRect = minAreaRect(contour);
// 外接正矩形(bounding box)
Rect bbox = minRect.boundingRect();
这在实际使用中意味着什么?在文本检测、车牌识别中,物体往往是倾斜的,RotatedRect 能给出更紧凑的包围框,减少背景干扰。
4.4 Range 的半开区间 [start, end) 约定与切片操作
别急,这个看似简单,但有一个容易踩坑的地方——半开区间。
OpenCV 的 Range 表示 [start, end),即包含 start,不包含 end。这和 Python 的切片规则一致,但和 MATLAB 的闭区间不同。
Range r(10, 50); // [10, 50) -> 包含索引 10..49,共40个元素
// 大小计算
int size = r.size(); // 40
// 特殊Range:表示全部
Range all_range = Range::all();
// 在Mat切片中的应用
Mat img = Mat::zeros(100, 100, CV_8UC3);
Mat roi = img(Range(20, 80), Range(30, 70)); // 行[20,80), 列[30,70)
// ROI 尺寸 = 60x40
// 交集运算
Range r1(10, 50);
Range r2(30, 70);
Range r_inter = r1 & r2; // [30, 50)
这在实际使用中意味着什么?半开区间的约定让切片操作更加安全——Range(a, b) + Range(b, c) = Range(a, c),不会出现重复或遗漏的元素。
5. 标量与颜色表示
5.1 Scalar_ 的四通道泛型存储与算术运算类型提升
现在我们已经了解了区域描述类型,接下来看看 OpenCV 如何表示颜色和标量值。
Scalar_<T> 是 Vec<T, 4> 的派生类,固定四通道。为什么是四通道?因为图像处理中常见的颜色空间(BGR、RGB、RGBA、HSV)最多需要四个分量。
Scalar s1(10, 20, 30, 40); // (B=10, G=20, R=30, A=40)
Scalar s2(5, 5, 5, 5);
// 算术运算
Scalar s_add = s1 + s2; // (15, 25, 35, 45)
Scalar s_mul = s1 * 2.0; // (20, 40, 60, 80)
// 点积(四通道对应相乘求和)
double dot_val = s1.dot(s2); // 10*5 + 20*5 + 30*5 + 40*5 = 500
类型提升:当 int 类型的 Scalar 与 double 运算时,结果会自动提升为 double:
Scalar s_int(1, 2, 3, 4);
Scalar s_double = s_int * 1.5; // 结果变为 double 类型: (1.5, 3.0, 4.5, 6.0)
5.2 常用颜色常量的跨空间定义(BGR/RGB/HSV)
OpenCV 默认使用 BGR 顺序,这和大多数图像库的 RGB 顺序相反。这是一个历史遗留问题,但已经成为约定。
// BGR 颜色常量
Scalar color_red(0, 0, 255); // BGR: 纯红
Scalar color_green(0, 255, 0); // BGR: 纯绿
Scalar color_blue(255, 0, 0); // BGR: 纯蓝
Scalar color_white(255, 255, 255); // BGR: 白色
// HSV 颜色空间
// Hue: 0-179 (OpenCV 将 0-360 映射到 0-179)
// Saturation: 0-255
// Value: 0-255
Scalar hsv_red(0, 255, 255); // 红色在HSV
Scalar hsv_green(60, 255, 255); // 绿色在HSV
Scalar hsv_blue(120, 255, 255); // 蓝色在HSV
// 颜色空间转换
Mat bgr_red(1, 1, CV_8UC3, color_red);
Mat hsv_converted;
cvtColor(bgr_red, hsv_converted, COLOR_BGR2HSV);
这在实际使用中意味着什么?在肤色检测、颜色跟踪中,HSV 空间比 BGR 更稳定,因为亮度(V)和颜色信息(H、S)分离了。
5.3 多通道 Scalar 与 Mat 逐元素运算的广播规则
如果画成图,Scalar 与 Mat 的运算就像一台广播站,把 Scalar 的每个通道值"广播"到 Mat 的对应通道上。
Mat img = Mat::ones(3, 3, CV_8UC3) * 100; // 所有像素值=100
// 逐通道相加:B+10, G+20, R+30
Scalar add_val(10, 20, 30);
Mat result = img + add_val;
// 单通道Scalar广播到所有通道
Scalar uniform_add(50); // 所有通道+50
Mat result2 = img + uniform_add;
// 逐元素乘法(注意溢出)
Scalar mul_val(1.2, 1.0, 0.8);
Mat result3 = img.mul(mul_val);
这在实际使用中意味着什么?广播规则让图像的亮度调整、白平衡校正变得极其简洁——一行代码就能完成全图的通道级调整。
6. 小型矩阵与栈上优化
6.1 Matx 固定维度矩阵:行列式/逆/转置的编译期优化
现在我们已经了解了标量和颜色,接下来看看 OpenCV 如何处理小型矩阵运算。
Matx<T, m, n> 是一个模板类,维度在编译期确定。这和 Mat(运行期确定维度)有本质区别。
// 3x3 浮点矩阵
Matx33f m33(1, 2, 3,
0, 1, 4,
5, 6, 0);
// 行列式
float det = m33.det();
// 逆矩阵
Matx33f m33_inv = m33.inv();
// 转置
Matx33f m33_t = m33.t();
// 矩阵乘法
Matx33f m33_2(1, 0, 0,
0, 2, 0,
0, 0, 3);
Matx33f m33_mul = m33 * m33_2;
// 解线性方程组 Ax = b
Vec3f b(1, 2, 3);
Vec3f x = m33.solve(b); // 使用LU分解求解
4x4 矩阵——3D 图形变换的标配
// 平移矩阵
Matx44f m44(1, 0, 0, 10,
0, 1, 0, 20,
0, 0, 1, 30,
0, 0, 0, 1);
这在实际使用中意味着什么?在相机标定、3D 重建中,Matx33f 表示内参矩阵,Matx44f 表示外参矩阵,编译期优化让这些高频运算速度提升数倍。
6.2 Vec 固定向量与跨通道数据映射(reshape 语义)
Vec<T, cn> 是 Matx<T, cn, 1> 的派生类——本质上是一个固定通道数的列向量。
// 像素表示
Vec3b pixel(100, 150, 200); // 3通道uchar (BGR像素)
Vec4f rgba(1.0f, 0.5f, 0.0f, 1.0f); // 4通道float (RGBA)
// 通道访问
uchar blue = pixel[0];
uchar green = pixel[1];
uchar red = pixel[2];
// 与Scalar的转换
Scalar s = pixel; // Vec3b -> Scalar (通道扩展为4)
// reshape语义: Vec可以看作小型Mat
Mat m_from_vec(pixel); // 1x1 CV_8UC3 Mat
// 向量运算
Vec3f v1(1, 2, 3);
Vec3f v2(4, 5, 6);
Vec3f v_add = v1 + v2;
float v_dot = v1.dot(v2); // 32
这在实际使用中意味着什么?Vec3b 是访问图像像素的最常用类型——img.at<Vec3b>(y, x) 返回一个像素的 BGR 值。
6.3 栈上对象相对于 Mat 的零堆分配性能优势
如果画成图,栈上分配就像把东西放在手边的桌子上,随拿随用;堆上分配就像去仓库取货,需要走一段路。
// 栈上分配 (零堆分配)
Point2f p_stack(100, 200); // 8 bytes on stack
Vec3b pixel_stack(100, 150, 200); // 3 bytes on stack
Matx33f m33_stack; // 36 bytes on stack
// Mat 是堆上分配 (动态内存)
Mat m_heap(1000, 1000, CV_8UC3); // 3MB on heap
性能优势场景:
- 实时图像处理中的像素级运算
- 3D 图形变换中的 4x4 矩阵乘法
- 特征点描述子的向量运算
- 颜色空间转换的逐像素计算
这在实际使用中意味着什么?在嵌入式设备或实时系统中,减少堆分配能显著降低延迟和内存碎片。
7. 辅助工具类
7.1 TermCriteria 的 MAX_ITER/MAX_EPS 组合终止条件
现在我们已经了解了基础数据类型,接下来看看 OpenCV 提供的"控制旋钮"——终止条件。
TermCriteria 用于控制迭代算法的终止。它就像汽车的"刹车系统"——告诉算法什么时候该停下来。
// 仅最大迭代次数
TermCriteria tc_iter(TermCriteria::MAX_ITER, 100, 0);
// 仅精度阈值
TermCriteria tc_eps(TermCriteria::EPS, 0, 1e-6);
// 组合条件: 满足任一即终止
TermCriteria tc_both(TermCriteria::MAX_ITER + TermCriteria::EPS, 100, 1e-6);
// 实际应用: K-means聚类
Mat data(100, 2, CV_32F);
randn(data, 0, 1);
Mat labels, centers;
double compactness = kmeans(data, 5, labels,
TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS,
100, 0.01),
10, KMEANS_RANDOM_CENTERS, centers);
这在实际使用中意味着什么?组合终止条件让算法既不会跑太久(MAX_ITER),也不会在已经收敛时继续浪费计算(EPS)。
7.2 PCA/SVD 的轻量级封装与特征分解接口
PCA(主成分分析)—— 找到数据最重要的方向
如果画成图,PCA 就像把一团散乱的点,找到一条能最好概括它们分布方向的直线。
// 生成示例数据
Mat data(100, 2, CV_32F);
randn(data, Scalar(50, 50), Scalar(20, 10));
// PCA 主成分分析
PCA pca(data, Mat(), PCA::DATA_AS_ROW, 2);
cout << "均值 = " << pca.mean << endl;
cout << "特征向量 =\n" << pca.eigenvectors << endl;
cout << "特征值 = " << pca.eigenvalues << endl;
// 投影到主成分空间
Mat projected;
pca.project(data.row(0), projected);
// 重构
Mat reconstructed;
pca.backProject(projected, reconstructed);
SVD(奇异值分解)—— 矩阵的"解剖刀"
Mat A(3, 3, CV_32F);
randu(A, -10, 10);
SVD svd(A, SVD::FULL_UV); // 完整SVD
cout << "U =\n" << svd.u << endl;
cout << "W (奇异值) = " << svd.w.t() << endl;
cout << "Vt =\n" << svd.vt << endl;
// 验证: A = U * W * Vt
Mat W_diag = Mat::zeros(3, 3, CV_32F);
W_diag.at<float>(0,0) = svd.w.at<float>(0);
W_diag.at<float>(1,1) = svd.w.at<float>(1);
W_diag.at<float>(2,2) = svd.w.at<float>(2);
Mat A_reconstructed = svd.u * W_diag * svd.vt;
// SVD 解线性方程组 Ax = b
Mat b(3, 1, CV_32F, {1, 2, 3});
Mat x_svd = svd.solve(b);
这在实际使用中意味着什么?PCA 用于降维和特征提取,SVD 用于求解病态方程组和矩阵低秩近似。
7.3 RNG 随机数生成器:种子复现与多种分布采样
RNG(Random Number Generator)—— 可复现的随机性
如果画成图,RNG 就像一个精密的骰子机器——给定相同的种子,它总是产生相同的序列。这在实验复现中至关重要。
// 确定性种子,保证可复现
uint64 seed = 12345;
RNG rng(seed);
// 均匀分布整数 [0, 100)
int val = rng.uniform(0, 100);
// 均匀分布浮点数 [0, 1)
float fval = rng.uniform(0.0f, 1.0f);
// 高斯分布 (正态分布)
double gval = rng.gaussian(1.0); // 标准差=1
// 种子复现验证
RNG rng2(seed);
// rng 和 rng2 将产生完全相同的序列
// 填充随机矩阵
Mat random_mat(3, 3, CV_32F);
rng.fill(random_mat, RNG::UNIFORM, 0.0f, 1.0f);
Mat gaussian_mat(3, 3, CV_32F);
rng.fill(gaussian_mat, RNG::NORMAL, 0.0f, 1.0f);
// 随机打乱数组
vector<int> indices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
randShuffle(indices, 1.0, &rng);
这在实际使用中意味着什么?在深度学习的数据增强、RANSAC 随机采样一致性算法中,可复现的随机性是实验对比的基础。
8. 完整代码汇总
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;
// ==================== 1.3.1 几何点与向量类型 ====================
void demo_point_operations() {
cout << "\n=== 1.3.1.1 Point 坐标运算重载与范数 ===" << endl;
Point2f p1(3.0f, 4.0f);
Point2f p2(1.0f, 2.0f);
Point2f p_add = p1 + p2;
Point2f p_sub = p1 - p2;
Point2f p_mul = p1 * 2.0f;
Point2f p_div = p1 / 2.0f;
float norm_l2 = norm(p1);
float norm_l1 = norm(p1, NORM_L1);
float norm_inf = norm(p1, NORM_INF);
cout << "p1 = " << p1 << ", p2 = " << p2 << endl;
cout << "p1 + p2 = " << p_add << endl;
cout << "p1 - p2 = " << p_sub << endl;
cout << "p1 * 2 = " << p_mul << endl;
cout << "L2 norm = " << norm_l2 << endl;
cout << "L1 norm = " << norm_l1 << endl;
cout << "L∞ norm = " << norm_inf << endl;
Point3f p3d(1.0f, 2.0f, 2.0f);
cout << "\nPoint3f = " << p3d << ", norm = " << norm(p3d) << endl;
}
void demo_dot_cross_angle() {
cout << "\n=== 1.3.1.2 点积/叉积与向量夹角 ===" << endl;
Point3f a(1.0f, 0.0f, 0.0f);
Point3f b(0.0f, 1.0f, 0.0f);
Point3f c(1.0f, 1.0f, 0.0f);
float dot_ab = a.dot(b);
float dot_ac = a.dot(c);
Point3f cross_ab = a.cross(b);
float angle_ab = acos(a.dot(b) / (norm(a) * norm(b))) * 180.0 / CV_PI;
float angle_ac = acos(a.dot(c) / (norm(a) * norm(c))) * 180.0 / CV_PI;
cout << "a·b = " << dot_ab << ", a×b = " << cross_ab << endl;
cout << "夹角(a,b) = " << angle_ab << "°" << endl;
cout << "夹角(a,c) = " << angle_ac << "°" << endl;
}
void demo_distance_projection() {
cout << "\n=== 1.3.1.3 点到线段距离与投影 ===" << endl;
Point2f p(5.0f, 5.0f);
Point2f line_start(0.0f, 0.0f);
Point2f line_end(10.0f, 0.0f);
Point2f AP = p - line_start;
Point2f AB = line_end - line_start;
float t = AP.dot(AB) / AB.dot(AB);
Point2f closest;
if (t < 0) closest = line_start;
else if (t > 1) closest = line_end;
else closest = line_start + AB * t;
float dist = norm(p - closest);
cout << "点 P = " << p << endl;
cout << "投影参数 t = " << t << endl;
cout << "最近点 = " << closest << endl;
cout << "点到线段距离 = " << dist << endl;
}
// ==================== 1.3.2 区域描述类型 ====================
void demo_size_operations() {
cout << "\n=== 1.3.2.1 Size_ 宽高语义 ===" << endl;
Size2f sz(800.0f, 600.0f);
cout << "Size = " << sz << endl;
cout << "面积 = " << sz.area() << endl;
cout << "宽高比 = " << sz.width / sz.height << endl;
cout << "缩放0.5 = " << sz * 0.5f << endl;
}
void demo_rect_operations() {
cout << "\n=== 1.3.2.2 Rect_ 操作运算 ===" << endl;
Rect r1(100, 100, 200, 150);
Rect r2(150, 120, 200, 200);
cout << "r1 = " << r1 << endl;
cout << "tl = " << r1.tl() << ", br = " << r1.br() << endl;
cout << "交集 = " << (r1 & r2) << endl;
cout << "并集 = " << (r1 | r2) << endl;
cout << "contains(200,200) = " << r1.contains(Point(200, 200)) << endl;
cout << "r1 + Point(50,30) = " << (r1 + Point(50, 30)) << endl;
}
void demo_rotated_rect() {
cout << "\n=== 1.3.2.3 RotatedRect ===" << endl;
RotatedRect rRect(Point2f(400, 300), Size2f(200, 100), 30.0f);
cout << "中心 = " << rRect.center << ", 尺寸 = " << rRect.size
<< ", 角度 = " << rRect.angle << "°" << endl;
Point2f vertices[4];
rRect.points(vertices);
for (int i = 0; i < 4; i++)
cout << "顶点[" << i << "] = " << vertices[i] << endl;
vector<Point> contour = {Point(100,100), Point(200,80),
Point(250,150), Point(180,200), Point(90,180)};
RotatedRect minRect = minAreaRect(contour);
cout << "\n最小外接矩形: 中心=" << minRect.center
<< ", 尺寸=" << minRect.size << endl;
}
void demo_range() {
cout << "\n=== 1.3.2.4 Range 半开区间 ===" << endl;
Range r(10, 50);
cout << "Range(10,50): size=" << r.size() << ", [" << r.start
<< ", " << r.end << ")" << endl;
Range r1(10, 50), r2(30, 70);
cout << "交集 = [" << (r1 & r2).start << ", " << (r1 & r2).end << ")" << endl;
}
// ==================== 1.3.3 标量与颜色表示 ====================
void demo_scalar_operations() {
cout << "\n=== 1.3.3.1 Scalar_ 四通道 ===" << endl;
Scalar s1(10, 20, 30, 40);
Scalar s2(5, 5, 5, 5);
cout << "s1 = " << s1 << endl;
cout << "s1 + s2 = " << (s1 + s2) << endl;
cout << "s1 * 2 = " << (s1 * 2.0) << endl;
cout << "s1·s2 = " << s1.dot(s2) << endl;
Scalar s_int(1, 2, 3, 4);
cout << "类型提升: " << (s_int * 1.5) << endl;
}
void demo_color_constants() {
cout << "\n=== 1.3.3.2 颜色常量 ===" << endl;
Scalar red(0, 0, 255), green(0, 255, 0), blue(255, 0, 0);
cout << "BGR 红=" << red << " 绿=" << green << " 蓝=" << blue << endl;
Mat bgr_red(1, 1, CV_8UC3, red);
Mat hsv;
cvtColor(bgr_red, hsv, COLOR_BGR2HSV);
cout << "BGR红转HSV = " << hsv.at<Vec3b>(0,0) << endl;
}
void demo_scalar_broadcast() {
cout << "\n=== 1.3.3.3 广播规则 ===" << endl;
Mat img = Mat::ones(2, 2, CV_8UC3) * 100;
Scalar add_val(10, 20, 30);
Mat result = img + add_val;
cout << "原始 = " << img.at<Vec3b>(0,0) << endl;
cout << "Scalar = " << add_val << endl;
cout << "相加后 = " << result.at<Vec3b>(0,0) << endl;
}
// ==================== 1.3.4 小型矩阵与栈上优化 ====================
void demo_matx_operations() {
cout << "\n=== 1.3.4.1 Matx 固定矩阵 ===" << endl;
Matx33f m33(1, 2, 3, 0, 1, 4, 5, 6, 0);
cout << "m33 =\n" << m33 << endl;
cout << "det = " << m33.det() << endl;
cout << "inv =\n" << m33.inv() << endl;
cout << "transpose =\n" << m33.t() << endl;
Vec3f b(1, 2, 3);
Vec3f x = m33.solve(b);
cout << "解 Ax=b, x = " << x << endl;
}
void demo_vec_operations() {
cout << "\n=== 1.3.4.2 Vec 固定向量 ===" << endl;
Vec3b pixel(100, 150, 200);
Vec3f v1(1, 2, 3), v2(4, 5, 6);
cout << "Vec3b = " << pixel << endl;
cout << "v1 + v2 = " << (v1 + v2) << endl;
cout << "v1·v2 = " << v1.dot(v2) << endl;
Scalar s = pixel;
cout << "Vec3b->Scalar = " << s << endl;
}
void demo_stack_vs_heap() {
cout << "\n=== 1.3.4.3 栈上对象性能优势 ===" << endl;
Point2f p(100, 200);
Vec3b px(100, 150, 200);
Matx33f m;
cout << "Point2f: " << sizeof(p) << " bytes (栈)" << endl;
cout << "Vec3b: " << sizeof(px) << " bytes (栈)" << endl;
cout << "Matx33f: " << sizeof(m) << " bytes (栈)" << endl;
cout << "Mat(1000x1000x3): " << (1000*1000*3/1024) << " KB (堆)" << endl;
}
// ==================== 1.3.5 辅助工具类 ====================
void demo_term_criteria() {
cout << "\n=== 1.3.5.1 TermCriteria ===" << endl;
TermCriteria tc(TermCriteria::MAX_ITER + TermCriteria::EPS, 100, 1e-6);
cout << "组合条件: maxCount=100 或 epsilon=1e-6" << endl;
Mat data(50, 2, CV_32F);
randn(data, 0, 1);
Mat labels, centers;
kmeans(data, 3, labels, tc, 5, KMEANS_RANDOM_CENTERS, centers);
cout << "K-means 中心点数 = " << centers.rows << endl;
}
void demo_pca_svd() {
cout << "\n=== 1.3.5.2 PCA/SVD ===" << endl;
Mat data(50, 2, CV_32F);
randn(data, Scalar(50, 50), Scalar(20, 10));
PCA pca(data, Mat(), PCA::DATA_AS_ROW, 2);
cout << "PCA 均值=" << pca.mean << endl;
cout << "特征值=" << pca.eigenvalues.t() << endl;
Mat A = (Mat_<float>(3,3) << 1, 2, 3, 4, 5, 6, 7, 8, 10);
SVD svd(A, SVD::FULL_UV);
cout << "\nSVD 奇异值=" << svd.w.t() << endl;
}
void demo_rng() {
cout << "\n=== 1.3.5.3 RNG 随机数 ===" << endl;
RNG rng(12345);
cout << "种子=12345" << endl;
cout << "uniform(0,100): ";
for (int i = 0; i < 5; i++) cout << rng.uniform(0, 100) << " ";
cout << endl;
RNG rng2(12345);
cout << "复现验证: ";
for (int i = 0; i < 5; i++) {
int v1 = rng.uniform(0, 100);
int v2 = rng2.uniform(0, 100);
cout << (v1 == v2 ? "✓" : "✗") << " ";
}
cout << endl;
}
// ==================== 主函数 ====================
int main() {
demo_point_operations();
demo_dot_cross_angle();
demo_distance_projection();
demo_size_operations();
demo_rect_operations();
demo_rotated_rect();
demo_range();
demo_scalar_operations();
demo_color_constants();
demo_scalar_broadcast();
demo_matx_operations();
demo_vec_operations();
demo_stack_vs_heap();
demo_term_criteria();
demo_pca_svd();
demo_rng();
return 0;
}
9. 闭环总结:这在实际使用中意味着什么?
现在我们已经走完了 OpenCV 基础数据类型的完整旅程。让我们回顾一下这些"原子"在真实世界中的角色:
| 类型 | 实际应用场景 |
|---|---|
Point_ | 像素坐标、特征点位置、3D 点云 |
Size_ | 图像尺寸、窗口大小、特征描述子维度 |
Rect_ | 目标检测边界框、ROI 裁剪区域 |
RotatedRect | 倾斜文本检测、车牌定位 |
Scalar_ | 颜色值、像素强度、多通道偏移量 |
Matx | 相机内参/外参矩阵、仿射变换 |
Vec | 像素值访问、特征描述子 |
TermCriteria | 迭代算法控制(K-means、MeanShift) |
PCA/SVD | 降维、特征提取、方程组求解 |
RNG | 数据增强、RANSAC、随机采样 |
推荐阅读:
- OpenCV 官方文档 - Basic structures
- OpenCV 4.x 教程
- “Learning OpenCV 3” by Adrian Kaehler & Gary Bradski
本文采用对话式技术写作风格,旨在降低学习焦虑。如有疑问,欢迎一起探讨!
648



被折叠的 条评论
为什么被折叠?



