OpenCV 入门(二):像素访问、ROI 与深浅拷贝

文章目录

  • [OpenCV 入门(二):像素访问、ROI 与深浅拷贝](#OpenCV 入门(二):像素访问、ROI 与深浅拷贝)
    • 前言
    • 一、坐标系统:先行后列
    • [二、读一个像素:`at<T>(y, x)`](#二、读一个像素:at<T>(y, x))
      • [2.1 类型参数 T 怎么选](#2.1 类型参数 T 怎么选)
      • [2.2 读取并打印](#2.2 读取并打印)
      • [2.3 `Vec3b` 是什么](#2.3 Vec3b 是什么)
    • [三、改一个像素:`at` 返回的是引用](#三、改一个像素:at 返回的是引用)
      • [3.1 改一块区域:双重循环](#3.1 改一块区域:双重循环)
      • [3.2 验证坐标方向:画一条对角线](#3.2 验证坐标方向:画一条对角线)
    • 四、ROI:从原图上"开一扇窗"
      • [4.1 `Rect(x, y, width, height)`](#4.1 Rect(x, y, width, height))
      • [4.2 ROI 不是复制,是"视图"](#4.2 ROI 不是复制,是"视图")
      • [4.3 ROI 的典型用途](#4.3 ROI 的典型用途)
    • [五、浅拷贝 vs 深拷贝:本篇最重要的部分](#五、浅拷贝 vs 深拷贝:本篇最重要的部分)
      • [5.1 `Mat` 的赋值是浅拷贝](#5.1 Mat 的赋值是浅拷贝)
      • [5.2 引用计数:为什么不会 double free](#5.2 引用计数:为什么不会 double free)
      • [5.3 一个关键区别:Mat 能表示"子区域"](#5.3 一个关键区别:Mat 能表示"子区域")
      • [5.4 深拷贝:`clone()` 和 `copyTo()`](#5.4 深拷贝:clone() 和 copyTo())
      • [5.5 一个常见的误解:`img(rect).clone()` 是多大?](#5.5 一个常见的误解:img(rect).clone() 是多大?)
      • [5.6 函数传参的坑](#5.6 函数传参的坑)
      • [5.7 没有写时复制(COW)](#5.7 没有写时复制(COW))
    • [六、性能提示:`at` 不便宜](#六、性能提示:at 不便宜)
    • 七、常见坑
    • 八、小结

OpenCV 入门(二):像素访问、ROI 与深浅拷贝

环境:Windows + Visual Studio 2022 + OpenCV 4.x,语言 C++17。

第一篇讲了图像是一张数字表格、Mat 是什么、怎么读图/显示/存图。这一篇进入真正动手的部分:怎么读写单个像素 ,以及 Mat 最反直觉的一点------赋值到底是复制数据,还是共享内存。

前言

上一篇结束时留下一个问题:图像既然是一张表格,那"改一个格子"该怎么写?

答案就是 at<T>(y, x)。但比 at 本身更重要的是它背后那套坐标约定和内存模型------坐标系反人类、赋值不复制数据,这两个坑几乎每个新手都踩过,而且踩了不一定报错,只会得到一张莫名其妙的图。

这一篇就把这两件事讲透。


一、坐标系统:先行后列

图像的行列和数学坐标系是反的:

复制代码
        x →(列,向右)
      ┌──────────────┐
    y │ (0,0)        │
    ↓ │              │
  (行,│              │
  向下)│              │
      └──────────────┘
  • 原点在左上角
  • x 向右(列方向)
  • y 向下(行方向)

而 at 的参数顺序是 (y, x) ------先行后列:

cpp 复制代码
img.at<Vec3b>(y, x)   // 第 y 行、第 x 列

数学里写 (x, y),OpenCV 写 (y, x)。写反了不会报错,只会取到另一个像素(相当于沿对角线翻转),调半天才发现。

记忆方法:Mat(rows, cols) 也是先高后宽,和 at(y, x) 一致。OpenCV 整体就是"先行后列"的风格。


二、读一个像素:at<T>(y, x)

2.1 类型参数 T 怎么选

图像类型 用哪个 T 说明
彩色 CV_8UC3 Vec3b 3 个 uchar,顺序 B、G、R
灰度 CV_8UC1 uchar 1 个字节,0~255
浮点单通道 CV_32F float
浮点三通道 CV_32FC3 Vec3f

T 写错(比如彩色图用 uchar)不会报错,只会读到第一个通道的一个字节 ------结果是垃圾值。所以 T 必须和 Mat::type() 对上。

2.2 读取并打印

cpp 复制代码
cv::Mat img = cv::imread("data/lena.jpg", cv::IMREAD_COLOR);
if (img.empty()) {
    std::cout << "读图失败" << std::endl;
    return -1;
}

int x = 200, y = 100;
cv::Vec3b p = img.at<cv::Vec3b>(y, x);   // 注意是 (y, x)

std::cout << "BGR = ("
          << (int)p[0] << ", "
          << (int)p[1] << ", "
          << (int)p[2] << ")" << std::endl;

输出:

复制代码
BGR = (118, 119, 209)

两个要点:

  1. p[0] 是 B,p[2] 是 R(BGR 顺序,第一篇讲过)。这组数里 R 最大,所以这个点是偏红的肤色。
  2. 打印必须 (int) 强转 。p[0] 是 uchar,直接 cout 会当成字符输出(118 对应字符 v)。

2.3 Vec3b 是什么

cpp 复制代码
typedef Vec<uchar, 3> Vec3b;   // 3 个 uchar 的小数组

它就是个定长 3 的数组,支持 p[0]、p[1]、p[2],也支持 p.val[0]。命名规则:

  • Vec3b = 3 通道,b yte(uchar)
  • Vec3f = 3 通道,float
  • Vec4b = 4 通道(带 alpha)

三、改一个像素:at 返回的是引用

cpp 复制代码
img.at<cv::Vec3b>(y, x) = cv::Vec3b(0, 0, 255);   // 纯红

这里能直接赋值,是因为 at 返回的是 Vec3b&(引用) ,不是值。这一点和 std::vector::operator[] 完全一样:

cpp 复制代码
std::vector<int> v(10);
v[0] = 1;        // 改的是容器里的元素

img.at<cv::Vec3b>(y, x) = ...;   // 改的是图像里的像素

注意和 std::vector<bool>、std::map::operator[] 这类返回代理对象的坑不同,Mat::at 返回的是货真价实的引用,没有隐藏行为。

3.1 改一块区域:双重循环

cpp 复制代码
// 左上角 40x40 涂成绿色
for (int yy = 0; yy < 40; ++yy) {
    for (int xx = 0; xx < 40; ++xx) {
        img.at<cv::Vec3b>(yy, xx) = cv::Vec3b(0, 255, 0);
    }
}
  • 外层是 y(行),内层是 x(列)------和内存布局一致,缓存友好
  • 内层循环变量变化最快,对应内存里连续的一段

如果只是想整块涂色,不用循环:img(cv::Rect(0, 0, 40, 40)).setTo(cv::Scalar(0, 255, 0));。后面讲 ROI 时会说到。

3.2 验证坐标方向:画一条对角线

cpp 复制代码
for (int i = 0; i < 200; ++i) {
    img.at<cv::Vec3b>(i, i) = cv::Vec3b(255, 255, 255);
}

(i, i) 意味着 x 和 y 同步增长,画出来的是从左上角往右下角的斜线。

如果这条线从右上往左下 ,说明你的 at 参数写反了。


四、ROI:从原图上"开一扇窗"

4.1 Rect(x, y, width, height)

cpp 复制代码
cv::Rect face(200, 150, 100, 100);   // 左上角 (200,150),宽 100,高 100
cv::Mat roi = img(face);

注意 Rect 的参数顺序是 (x, y, 宽, 高) ,和 at(y, x) 又反过来了------Rect 用的是数学习惯。这是 OpenCV 里另一处容易写反的地方。

4.2 ROI 不是复制,是"视图"

cpp 复制代码
roi.setTo(cv::Scalar(255, 0, 0));   // 把 roi 涂蓝

此时 img 上对应那块区域也会变蓝。

因为 img(face) 只创建了一个新的表头,里面记录:

  • 数据指针 → 指向原图那块内存的中间某个位置
  • rows / cols → 100 / 100
  • step → 仍然是原图的行字节数

像素数据一个字节都没复制。所以 roi 就是 img 的一个"窗口",改窗口就是改原图。

4.3 ROI 的典型用途

用途 写法
只看某个区域 cv::imshow("face", img(cv::Rect(...)))
只处理某个区域 cv::GaussianBlur(img(rect), dst, ...)
往原图贴东西 small.copyTo(img(rect));

这也是 OpenCV 高效的原因之一:切 ROI 是 O(1) 的,move 一个表头就行,不用拷贝几十万像素。


五、浅拷贝 vs 深拷贝:本篇最重要的部分

5.1 Mat 的赋值是浅拷贝

cpp 复制代码
cv::Mat a = img;    // 浅拷贝:共享同一块像素内存
a.setTo(0);         // img 也全黑了

Mat 内部结构是 "表头 + 数据块":

复制代码
a ──┐
    ├──→ [表头: rows/cols/step/type] ──→ [数据块], 引用计数 = 2
img ┘

赋值只复制表头,引用计数 +1,像素数据一个字节都不动。

5.2 引用计数:为什么不会 double free

数据块旁边有个计数器:

  • 拷贝一次 → +1
  • 析构一个 Mat → -1
  • 减到 0 才真正释放内存

所以浅拷贝是安全的,不需要你手动管理。这一点和 std::shared_ptr 完全一致:

std::shared_ptr<T> cv::Mat
拷贝 引用计数 +1 引用计数 +1,只复制表头
最后一个析构 释放对象 引用计数归零才 delete[]
改所指内容 *p = x 影响所有持有者 img.at<>(...) = x 影响所有持有者
改指向 p = other 不影响别人 img = other 不影响别人

5.3 一个关键区别:Mat 能表示"子区域"

shared_ptr 只能指向同一个对象 。Mat 是"表头 + 数据",所以多个 Mat 可以是同一块内存的不同视图:

cpp 复制代码
cv::Mat roi = img(cv::Rect(200, 150, 100, 100));
// 同一个数据块,但 rows/cols/数据指针都不同

这就是 Mat 必须自己实现引用计数、而不能直接套 shared_ptr 的原因。

5.4 深拷贝:clone() 和 copyTo()

想真正独立,必须显式拷贝:

cpp 复制代码
cv::Mat b = img.clone();          // 深拷贝,新建一块内存

cv::Mat c;
img.copyTo(c);                    // 深拷贝,等价于 clone()

cv::Mat d;
img.copyTo(d, mask);              // 深拷贝 + 掩码,只复制 mask 非 0 的位置

两者的区别很小:

  • clone():永远新分配内存
  • copyTo():如果目标 Mat 的尺寸和类型正好合适 ,会复用已有内存,省一次分配。视频循环里每帧尺寸固定,用 copyTo 更省

5.5 一个常见的误解:img(rect).clone() 是多大?

cpp 复制代码
cv::Mat face = img(cv::Rect(200, 150, 100, 100)).clone();
// face 是 100x100,不是整图

因为 clone() 复制的是"你手上这个 Mat 的尺寸" ,而 img(rect) 本身就已经是 100×100 的视图了。

想把整图复制一份、再改其中一块,顺序要反过来:

cpp 复制代码
cv::Mat full = img.clone();                  // ① 整图深拷贝
full(cv::Rect(200, 150, 100, 100)).setTo(cv::Scalar(0, 0, 255));   // ② 在副本上开窗改

打印出来验证一下最直观:

cpp 复制代码
std::cout << img.cols  << " x " << img.rows  << std::endl;   // 512 x 512
std::cout << face.cols << " x " << face.rows << std::endl;   // 100 x 100

5.6 函数传参的坑

Mat 按值传参很便宜(只复制表头),但函数里改像素会直接影响调用者:

cpp 复制代码
void paintRed(cv::Mat m)                 // 按值传,只拷表头
{
    m.setTo(cv::Scalar(0, 0, 255));      // 改的是共享像素 -> 调用者的图也变红
}

void repoint(cv::Mat m)
{
    m = cv::Mat::zeros(10, 10, CV_8UC3); // 只改自己这个表头 -> 调用者毫无影响
}

规则和 shared_ptr 一样:

改内容会传染,改指向不会。

如果不想让函数改到原图,函数内部先 clone(),或者干脆用 const cv::Mat& 并在接口约定里说明"不修改像素"(注意 const 只保证不能重新赋值表头,不阻止 setTo,所以约定仍然重要)。

5.7 没有写时复制(COW)

和 Qt 的 QImage、QString 不同,OpenCV 的 Mat 不做写时复制:

复制代码
Qt:    拷贝 -> 共享 -> 一写就偷偷复制一份(COW)
OpenCV:拷贝 -> 共享 -> 一写就真的改到共享内存

从 Qt 转过来的人特别容易在这里翻车------以为改的是副本,其实改的是原图。


六、性能提示:at 不便宜

at<T>(y, x) 每次调用都要算地址,Debug 下还有边界检查。单点读写完全没问题,但逐像素遍历整张图时,它可能是瓶颈。

需要极致性能时,用行指针:

cpp 复制代码
for (int y = 0; y < img.rows; ++y) {
    cv::Vec3b* row = img.ptr<cv::Vec3b>(y);   // 取第 y 行的首地址
    for (int x = 0; x < img.cols; ++x) {
        row[x][0] = 0;   // 直接按指针偏移访问
    }
}

三种访问方式的速度大致是:

复制代码
ptr<T>()  >  at<T>()  >>  迭代器(实际上迭代器也很快,但灵活性差)

原则:先用 at 把逻辑写对,确认是性能热点再换 ptr。 不要过早优化。

更省事的做法是用 OpenCV 自带的函数(cv::LUT、cv::threshold、cv::convertTo),它们的循环是 SIMD 优化的,通常比手写循环快一个数量级。


七、常见坑

  1. at(y, x) 先行后列。 写成 at(x, y) 不报错,只是取到对角线上的另一个像素。
  2. Rect(x, y, w, h) 反过来是 (x, y, 宽, 高)。 和 at 的顺序不一致,靠 Rect 用的是数学习惯。
  3. T 必须和 type() 对应。 彩色图用 uchar 不报错,只读到第一个通道的一个字节。
  4. at 返回引用,赋值直接改原图。 想保留原图先 clone()。
  5. Mat a = b 是浅拷贝。 改 a 会改 b,和 shared_ptr 一样。
  6. ROI 是视图,不是复制。 改 ROI 就是改原图。
  7. img(rect).clone() 得到的是 rect 大小,不是整图。
  8. Mat 没有 COW。 别拿 Qt 的 QImage 经验套。
  9. 函数按值收 Mat,改像素会传染给调用者。
  10. 打印 uchar 要 (int) 强转,否则输出字符。

八、小结

概念 一句话
坐标 原点左上,x 向右,y 向下
at<T>(y, x) 先行后列;T 要匹配通道类型
Vec3b 3 个 uchar,顺序 B、G、R
at 的返回值 引用,赋值即改原图
Rect(x,y,w,h) 先坐标后宽高,和 at 顺序相反
ROI 原图上的窗口,共享内存,O(1)
浅拷贝 = / (rect),共享内存,引用计数管理
深拷贝 clone() / copyTo(),独立内存
copyTo(dst, mask) 只复制 mask 非 0 的像素
性能 先用 at,热点再换 ptr
相关推荐
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(116):Hybrid Memory——用互补策展与离线巩固构建长期记忆
论文阅读·人工智能·学习·开源·github
xiaoyunxuexibiji1 小时前
采购协同管理:从备货计划、进度确认到到货签收
大数据·运维·人工智能
数据库小学妹1 小时前
AI Agent连数据库怎么控权限?只读账号的4个缺口
数据库·人工智能·安全·数据库权限·审计·ai数据库·只读账号
Rocky Ding*2 小时前
【三年面试五年模拟】2026-10-26 携程 AI Agent一面面经全解析
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·ai agent
Zhao-Jichao2 小时前
AI Prompting for Everyone 课程笔记 - 02 预训练知识 Pretrained knowledge
人工智能
ai_xiaogui2 小时前
PanelAI项目实战:从AI一键生成到企业级AI基础设施操作系统PPT手搓复盘与架构落地
人工智能·架构·一键部署·panelai·企业级ai基础设施操作系统·ai智能底层操作系统·算力变现
lucas_AI2 小时前
NPU 算力不再注水:拿量产世界模型实测芯片,700 TOPS 里 98.7% 真能干活
人工智能
CAIE研习社2 小时前
30岁以后学习AI应用,证书能起到什么作用?
人工智能·学习
Dawson Zhu2 小时前
【Runtime推理引擎①】Runtime架构设计:从Session到Graph Executor的完整骨架
人工智能·语言模型·架构·aigc·agi