【C++指针】 指针操作二维数组对opencv灰度图做反色
图像在计算机中本质上是一个二维数组(灰度图)或三维数组(彩色图)。使用指针操作图像数据是图像处理中最基础也是最重要的技能,它让我们能够直接访问和修改内存中的像素数据,实现高效的图像处理算法。字节对齐是指数据在内存中存放的起始地址必须是某个值的倍数(通常是2、4、8等)。这是为了CPU访问效率而设计的硬件要求。图像本质:图像是内存中的二维数组指针操作:通过img.data可以访问原始像素数据索引计算
·
📚 前言
图像在计算机中本质上是一个二维数组(灰度图)或三维数组(彩色图)。使用指针操作图像数据是图像处理中最基础也是最重要的技能,它让我们能够直接访问和修改内存中的像素数据,实现高效的图像处理算法。
#include <opencv2/opencv.hpp>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
auto img = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
int height = img.rows; //hang
int width = img.cols; //lie
cout << "height = " << height << " width = " << width << endl;
cout << "img.elemSize() = " << img.elemSize() << endl; //单个像素
imshow("test1", img);
waitKey(5000);
//做反色
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
auto c = img.data[i * width + j];
img.data[i * width + j] = 255 - c;
}
}
imshow("test2", img);
waitKey(5000);
return 0;
}

第一章:代码逐行解析
1.1 头文件引入
#include <opencv2/opencv.hpp> // OpenCV库,用于图像处理
#include <iostream> // 输入输出流
知识点:
- OpenCV是计算机视觉领域最流行的开源库
opencv.hpp包含了OpenCV的主要功能模块
1.2 读取图像
auto img = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
参数说明:
- 第一个参数:图像文件路径
- 第二个参数:读取模式
IMREAD_GRAYSCALE:以灰度模式读取(单通道)IMREAD_COLOR:以彩色模式读取(3通道BGR)IMREAD_UNCHANGED:保留原格式(包含Alpha通道)
返回值:cv::Mat 对象,OpenCV中用于存储图像的数据结构
1.3 获取图像尺寸
int height = img.rows; // 图像高度(行数)
int width = img.cols; // 图像宽度(列数)
cout << "height = " << height << " width = " << width << endl;
cout << "img.elemSize() = " << img.elemSize() << endl; // 单个像素的字节数
Mat对象的重要属性:
| 属性 | 含义 | 示例值 |
|---|---|---|
rows |
图像行数(高度) | 480 |
cols |
图像列数(宽度) | 640 |
elemSize() |
每个像素的字节数 | 灰度图=1,RGB图=3 |
total() |
总像素数 | rows × cols |
data |
指向图像数据的指针 | uchar*类型 |
1.4 显示原始图像
imshow("test1", img); // 显示图像,窗口标题为"test1"
waitKey(5000); // 等待5000毫秒(5秒)
重要:waitKey() 不仅用于等待,还负责处理窗口事件。如果不调用,图像窗口可能无法正常显示。
1.5 通过指针操作图像数据(反色处理)
// 做反色
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
auto c = img.data[i * width + j]; // 读取像素值
img.data[i * width + j] = 255 - c; // 反色后写入
}
}
指针操作的深入理解
图像数据在内存中的存储方式:
内存地址: [像素0,0] [像素0,1] [像素0,2] ... [像素0,width-1]
[像素1,0] [像素1,1] [像素1,2] ... [像素1,width-1]
...
[像素height-1,0] ... [像素height-1,width-1]
二维数组到一维索引的映射:
第i行第j列的像素位置 = i * width + j
为什么这样计算?
- 每行有
width个像素 - 要访问第
i行,需要跳过前面的i行,每行width个像素 - 再加上本行中的第
j个偏移
图解:
行0: [0] [1] [2] [3] ... [width-1]
行1: [width] [width+1] [width+2] ... [2*width-1]
行2: [2*width] [2*width+1] ... [3*width-1]
...
行i: [i*width] [i*width+1] ... [i*width + j]
1.6 显示处理后的图像
imshow("test2", img);
waitKey(5000);
第二章:字节对齐问题深度解析(拓展)
2.1 什么是字节对齐?
字节对齐是指数据在内存中存放的起始地址必须是某个值的倍数(通常是2、4、8等)。这是为了CPU访问效率而设计的硬件要求。
2.2 OpenCV中的字节对齐
代码中确实缺少了对字节对齐的处理!虽然在这个简单的例子中可能不会出错,但在更复杂的图像处理中,这会导致严重问题。
OpenCV默认的对齐方式
Mat img = imread("image.jpg");
cout << "step = " << img.step << endl; // 实际一行的字节数(包含填充)
cout << "cols = " << img.cols << endl; // 理论上的列数
cout << "step/cols = " << img.step / img.elemSize() << endl; // 实际有效列数
关键概念:
step:实际一行占用的字节数(包含为了对齐而填充的字节)cols×elemSize():理论上一行需要的字节数- 当
step>cols × elemSize()时,说明有字节对齐填充
2.3 为什么需要字节对齐?
// 假设图像宽度=3,但按4字节对齐
// 内存布局可能是:
// 行0: [像素0] [像素1] [像素2] [填充字节]
// 行1: [像素3] [像素4] [像素5] [填充字节]
// 行2: [像素6] [像素7] [像素8] [填充字节]
对齐的原因:
- CPU访问效率:许多CPU在访问对齐的地址时更快
- 硬件要求:某些平台要求特定类型的对齐
- 缓存性能:对齐可以减少缓存行分裂
2.4 正确的指针访问方式
错误的方式(你当前的代码):
// ❌ 假设没有对齐填充
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
img.data[i * width + j] = 255 - img.data[i * width + j];
}
}
正确的方式(考虑字节对齐):
// ✅ 使用step考虑对齐
for (int i = 0; i < height; i++) {
uchar* row_ptr = img.data + i * img.step; // 获取第i行的起始指针
for (int j = 0; j < width; j++) {
row_ptr[j] = 255 - row_ptr[j]; // 直接操作行指针
}
}
// 或者使用ptr模板函数(推荐)
for (int i = 0; i < height; i++) {
uchar* row_ptr = img.ptr<uchar>(i); // OpenCV提供的安全方式
for (int j = 0; j < width; j++) {
row_ptr[j] = 255 - row_ptr[j];
}
}
2.5 验证字节对齐的代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
// 创建不同宽度的图像
Mat img1(100, 3, CV_8UC1); // 宽度3的灰度图
Mat img2(100, 4, CV_8UC1); // 宽度4的灰度图
Mat img3(100, 3, CV_8UC3); // 宽度3的彩色图
cout << "=== 灰度图 宽度3 ===" << endl;
cout << "cols = " << img1.cols << endl;
cout << "elemSize = " << img1.elemSize() << endl;
cout << "理论一行字节数 = " << img1.cols * img1.elemSize() << endl;
cout << "实际step = " << img1.step << endl;
cout << "是否有填充 = " << (img1.step > img1.cols * img1.elemSize() ? "是" : "否") << endl;
cout << "\n=== 灰度图 宽度4 ===" << endl;
cout << "cols = " << img2.cols << endl;
cout << "理论一行字节数 = " << img2.cols * img2.elemSize() << endl;
cout << "实际step = " << img2.step << endl;
cout << "\n=== 彩色图 宽度3 ===" << endl;
cout << "cols = " << img3.cols << endl;
cout << "elemSize = " << img3.elemSize() << endl;
cout << "理论一行字节数 = " << img3.cols * img3.elemSize() << endl;
cout << "实际step = " << img3.step << endl;
return 0;
}
可能的输出:
=== 灰度图 宽度3 ===
cols = 3
elemSize = 1
理论一行字节数 = 3
实际step = 4 // 填充了1字节!
是否有填充 = 是
=== 灰度图 宽度4 ===
cols = 4
理论一行字节数 = 4
实际step = 4 // 刚好4字节,无需填充
=== 彩色图 宽度3 ===
cols = 3
elemSize = 3
理论一行字节数 = 9
实际step = 12 // 填充了3字节!
第三章:改进后的完整代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
// 1. 读取图像
Mat img = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
if (img.empty()) {
cout << "无法加载图像!" << endl;
return -1;
}
// 2. 显示图像信息
int height = img.rows;
int width = img.cols;
cout << "图像尺寸: " << height << " x " << width << endl;
cout << "像素类型: " << img.type() << endl;
cout << "每个像素字节数: " << img.elemSize() << endl;
cout << "一行理论字节数: " << width * img.elemSize() << endl;
cout << "一行实际字节数(step): " << img.step << endl;
cout << "是否有字节对齐填充: " << (img.step > width * img.elemSize() ? "是" : "否") << endl;
// 3. 显示原始图像
imshow("原始图像", img);
waitKey(2000);
// 4. 方法1:使用行指针(推荐,考虑对齐)
cout << "\n使用方法1:行指针访问(考虑对齐)..." << endl;
for (int i = 0; i < height; i++) {
uchar* row_ptr = img.ptr<uchar>(i); // 获取第i行的起始指针
for (int j = 0; j < width; j++) {
row_ptr[j] = 255 - row_ptr[j]; // 反色处理
}
}
// 显示反色后的图像
imshow("反色图像-方法1", img);
waitKey(2000);
// 5. 恢复原图(重新读取)
img = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
// 6. 方法2:使用data指针(需要手动处理对齐)
cout << "使用方法2:data指针访问(手动处理对齐)..." << endl;
for (int i = 0; i < height; i++) {
uchar* row_ptr = img.data + i * img.step; // 使用step跳行
for (int j = 0; j < width; j++) {
row_ptr[j] = 255 - row_ptr[j];
}
}
// 显示反色后的图像
imshow("反色图像-方法2", img);
waitKey(2000);
// 7. 方法3:直接索引(OpenCV内部处理对齐)
cout << "使用方法3:at方法(最安全,但最慢)..." << endl;
img = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
img.at<uchar>(i, j) = 255 - img.at<uchar>(i, j);
}
}
imshow("反色图像-方法3", img);
waitKey(5000);
// 8. 性能对比(简单计时)
cout << "\n=== 性能对比 ===" << endl;
Mat test1 = imread("C:\\Users\\zzzzzm\\Pictures\\OIP.png", IMREAD_GRAYSCALE);
Mat test2 = test1.clone();
Mat test3 = test1.clone();
double t1 = (double)getTickCount();
// 方法1:ptr方式
for (int i = 0; i < height; i++) {
uchar* row = test1.ptr<uchar>(i);
for (int j = 0; j < width; j++) {
row[j] = 255 - row[j];
}
}
t1 = ((double)getTickCount() - t1) / getTickFrequency();
cout << "ptr方法耗时: " << t1 * 1000 << " ms" << endl;
double t2 = (double)getTickCount();
// 方法2:data+step方式
for (int i = 0; i < height; i++) {
uchar* row = test2.data + i * test2.step;
for (int j = 0; j < width; j++) {
row[j] = 255 - row[j];
}
}
t2 = ((double)getTickCount() - t2) / getTickFrequency();
cout << "data+step方法耗时: " << t2 * 1000 << " ms" << endl;
double t3 = (double)getTickCount();
// 方法3:at方式
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
test3.at<uchar>(i, j) = 255 - test3.at<uchar>(i, j);
}
}
t3 = ((double)getTickCount() - t3) / getTickFrequency();
cout << "at方法耗时: " << t3 * 1000 << " ms" << endl;
waitKey(0);
return 0;
}
第四章:三种访问方式的对比
| 访问方式 | 语法 | 安全性 | 性能 | 是否考虑对齐 |
|---|---|---|---|---|
ptr<T>(i)[j] |
img.ptr<uchar>(i)[j] |
⭐⭐⭐ | ⭐⭐⭐ | ✅ 自动处理 |
data + i*step + j |
img.data[i*step + j] |
⭐⭐ | ⭐⭐⭐ | ✅ 手动处理 |
at<T>(i,j) |
img.at<uchar>(i,j) |
⭐⭐⭐ | ⭐ | ✅ 自动处理 |
选择建议:
- 追求性能:使用
ptr<T>(i)方式 - 确保安全:使用
at<T>(i,j)方式 - 需要底层控制:使用
data + step方式
第五章:练习与思考
练习1:检查你的图像是否有对齐
Mat img = imread("your_image.jpg");
cout << "step: " << img.step << endl;
cout << "cols * elemSize: " << img.cols * img.elemSize() << endl;
cout << "差值: " << img.step - img.cols * img.elemSize() << " 字节" << endl;
练习2:处理彩色图像
Mat color_img = imread("image.jpg", IMREAD_COLOR);
int channels = color_img.channels(); // 通道数=3
for (int i = 0; i < color_img.rows; i++) {
Vec3b* row = color_img.ptr<Vec3b>(i); // 使用Vec3b类型
for (int j = 0; j < color_img.cols; j++) {
// BGR格式:row[j][0]=B, row[j][1]=G, row[j][2]=R
row[j][0] = 255 - row[j][0]; // B通道反色
row[j][1] = 255 - row[j][1]; // G通道反色
row[j][2] = 255 - row[j][2]; // R通道反色
}
}
练习3:思考题
- 为什么OpenCV要进行字节对齐?
- 如果忽略对齐,直接使用
i*width + j访问,什么情况下会出错? - 如何判断一个图像是否有对齐填充?
总结
- 图像本质:图像是内存中的二维数组
- 指针操作:通过
img.data可以访问原始像素数据 - 索引计算:第i行j列的位置 = i × 每行字节数 + j × 每像素字节数
- 字节对齐:使用
img.step而不是img.cols × elemSize() - 最佳实践:优先使用
img.ptr<T>(i)安全高效
处理图像时,永远使用 step 来计算行偏移,这样才能保证代码在所有平台上正确运行!
更多推荐
所有评论(0)