📚 前言

图像在计算机中本质上是一个二维数组(灰度图)或三维数组(彩色图)。使用指针操作图像数据是图像处理中最基础也是最重要的技能,它让我们能够直接访问和修改内存中的像素数据,实现高效的图像处理算法。


#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] [填充字节]

对齐的原因

  1. CPU访问效率:许多CPU在访问对齐的地址时更快
  2. 硬件要求:某些平台要求特定类型的对齐
  3. 缓存性能:对齐可以减少缓存行分裂

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:思考题

  1. 为什么OpenCV要进行字节对齐?
  2. 如果忽略对齐,直接使用 i*width + j 访问,什么情况下会出错?
  3. 如何判断一个图像是否有对齐填充?

总结

  1. 图像本质:图像是内存中的二维数组
  2. 指针操作:通过 img.data 可以访问原始像素数据
  3. 索引计算:第i行j列的位置 = i × 每行字节数 + j × 每像素字节数
  4. 字节对齐:使用 img.step 而不是 img.cols × elemSize()
  5. 最佳实践:优先使用 img.ptr<T>(i) 安全高效

处理图像时,永远使用 step 来计算行偏移,这样才能保证代码在所有平台上正确运行!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐