2. OpenCV基础(一):图像读取与显示、属性、保存与像素操作
好,咱们正式开始动手写代码。这一章是OpenCV的入门基本功,说白了就是怎么把一张图片从硬盘里拽出来,看看它长什么样,改几个像素再存回去。这些操作在YOLO项目中几乎天天用——你想想看,训练前要检查数据集,推理时要显示检测结果,后处理要裁剪ROI,全都绕不开这几个函数。
我个人习惯把这一套流程叫做「图像生命周期管理」。别笑,名字起得大,但核心就四个动作:读、看、改、存。咱们一个一个来。
2.1 图像读取:imread
imread 是OpenCV的入口函数。它的签名很简单:
cv::Mat cv::imread(const String& filename, int flags = IMREAD_COLOR)
第一个参数是文件路径,第二个参数是读取模式。模式这块容易踩坑,我列个表:
| 标志常量 | 数值 | 含义 |
|---|---|---|
IMREAD_COLOR |
1 | 默认模式,加载为BGR三通道彩色图,忽略Alpha通道 |
IMREAD_GRAYSCALE |
0 | 加载为单通道灰度图 |
IMREAD_UNCHANGED |
-1 | 保留原图所有通道(含Alpha) |
IMREAD_ANYCOLOR |
4 | 按原图色彩加载,但会转成8位 |
IMREAD_COLOR 加载PNG透明图,结果Alpha通道直接丢了。后来做YOLO分割任务时,发现mask边缘有锯齿,查了半天才发现是读取时把透明度信息扔了。如果你需要保留透明通道,务必用 IMREAD_UNCHANGED。
代码示例:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 彩色读取
cv::Mat img_color = cv::imread("cat.jpg", cv::IMREAD_COLOR);
// 灰度读取
cv::Mat img_gray = cv::imread("cat.jpg", cv::IMREAD_GRAYSCALE);
// 保留原通道
cv::Mat img_unchanged = cv::imread("cat.png", cv::IMREAD_UNCHANGED);
if (img_color.empty()) {
std::cerr << "图片没找到,检查路径!" << std::endl;
return -1;
}
return 0;
}
注意那个 empty() 检查。我见过太多新手直接拿空Mat往下传,程序崩了还不知道为什么。路径写错、文件名大小写不对、中文路径——这些都是常见原因。
2.2 图像显示:imshow + waitKey
读进来之后,总得看一眼吧?imshow 负责把Mat渲染到窗口上。
cv::imshow("窗口标题", img_color);
cv::waitKey(0); // 等待按键,0表示无限等待
这里有个关键点:imshow 必须搭配 waitKey 才能正常显示。为什么?因为OpenCV的GUI事件循环是靠 waitKey 驱动的。你不调用它,窗口一闪就没了,或者干脆不显示。
waitKey(30) 表示等待30毫秒,常用于视频流显示。返回值是按键的ASCII码,比如按 'q' 退出:if (cv::waitKey(1) == 'q') break;
另外,imshow 默认把图像缩放到适应窗口大小。如果你想固定窗口尺寸,可以用 cv::namedWindow 配合 WINDOW_NORMAL 标志:
cv::namedWindow("可缩放窗口", cv::WINDOW_NORMAL);
cv::imshow("可缩放窗口", img_color);
cv::waitKey(0);
2.3 图像属性:shape 与 dtype
拿到Mat之后,第一件事就是看它的属性。OpenCV的Mat有三个核心属性:
- rows:图像高度(像素行数)
- cols:图像宽度(像素列数)
- channels():通道数(灰度=1,BGR=3,BGRA=4)
- type():数据类型,比如 CV_8UC3 表示8位无符号三通道
- depth():每个通道的位深
代码示例:
std::cout << "图像尺寸: " << img_color.rows << " x " << img_color.cols << std::endl;
std::cout << "通道数: " << img_color.channels() << std::endl;
std::cout << "数据类型: " << img_color.type() << std::endl; // 输出16,对应CV_8UC3
你可能会问:为什么OpenCV用BGR而不是RGB?历史原因——早期Windows的BMP格式就是BGR,OpenCV沿用了这个约定。所以用 imshow 显示没问题,但如果你用 matplotlib 显示,记得转成RGB:cv::cvtColor(img, img_rgb, cv::COLOR_BGR2RGB)。
2.4 图像保存:imwrite
处理完图像,要存盘就用 imwrite。它的签名:
bool cv::imwrite(const String& filename, InputArray img, const std::vector<int>& params = std::vector<int>())
第三个参数是编码参数,我常用的几个:
| 编码参数 | 作用 | 示例 |
|---|---|---|
IMWRITE_JPEG_QUALITY |
JPEG压缩质量,0-100 | params.push_back(95); |
IMWRITE_PNG_COMPRESSION |
PNG压缩级别,0-9 | params.push_back(3); |
IMWRITE_WEBP_QUALITY |
WebP质量,0-100 | params.push_back(80); |
std::vector<int> params;
params.push_back(cv::IMWRITE_JPEG_QUALITY);
params.push_back(95);
bool success = cv::imwrite("output.jpg", img_color, params);
if (!success) {
std::cerr << "保存失败!检查目录权限。" << std::endl;
}
2.5 像素操作与ROI提取
终于到了最核心的部分。在YOLO里,你要频繁地操作像素——画检测框、提取目标区域、做数据增强。OpenCV提供了两种方式:
2.5.1 直接像素访问
最直观的方法是用 at<T>:
// 读取(10, 20)位置的BGR值
cv::Vec3b pixel = img_color.at<cv::Vec3b>(10, 20);
int blue = pixel[0];
int green = pixel[1];
int red = pixel[2];
// 修改该像素为红色
img_color.at<cv::Vec3b>(10, 20) = cv::Vec3b(0, 0, 255);
注意:at<T> 的行列顺序是 (row, col),也就是 (y, x)。这和数学坐标系不一样,初学者经常搞反。
对于灰度图:
uchar val = img_gray.at<uchar>(10, 20);
img_gray.at<uchar>(10, 20) = 128;
at<T> 每次调用都会做边界检查,在循环里逐像素操作时非常慢。我曾经在一个1080p图像上逐像素遍历,花了200多毫秒。后来改用指针方式,直接降到5毫秒。所以大批量操作时,用 ptr 或 data 指针。
指针方式示例:
for (int r = 0; r < img_color.rows; r++) {
cv::Vec3b* row_ptr = img_color.ptr<cv::Vec3b>(r);
for (int c = 0; c < img_color.cols; c++) {
row_ptr[c][0] = 255; // 把这一行所有像素的蓝色通道设为255
}
}
2.5.2 ROI提取
ROI(Region of Interest)是YOLO里最常用的操作之一。比如你检测到一只猫,想把这个区域单独拿出来做分类,就需要ROI。
OpenCV用 cv::Rect 定义矩形区域:
// 定义ROI:左上角(100, 50),宽200,高150
cv::Rect roi(100, 50, 200, 150);
cv::Mat img_roi = img_color(roi); // 浅拷贝,共享内存
// 如果想独立拷贝一份
cv::Mat img_roi_copy = img_color(roi).clone();
这里有个重要概念:浅拷贝 vs 深拷贝。直接用 () 运算符得到的ROI和原图共享数据内存,修改ROI会影响原图。用 clone() 或 copyTo() 才会开辟新内存。
我画个图帮你理解这个关系:
ROI的典型应用场景:
- YOLO推理后处理:从检测框坐标提取目标区域,做二次分类或OCR
- 数据增强:随机裁剪图像的一部分作为训练样本
- 图像拼接:把多个ROI贴到一张大图上
完整示例:读取图片,提取ROI,修改像素,保存结果。
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("cat.jpg");
if (img.empty()) return -1;
// 提取左上角100x100区域
cv::Rect roi(0, 0, 100, 100);
cv::Mat roi_img = img(roi).clone(); // 深拷贝,独立操作
// 把ROI区域全部变蓝
roi_img = cv::Scalar(255, 0, 0);
// 在原图相同位置画一个红色矩形框
cv::rectangle(img, roi, cv::Scalar(0, 0, 255), 2);
cv::imshow("原图", img);
cv::imshow("ROI", roi_img);
cv::waitKey(0);
cv::imwrite("result.jpg", img);
return 0;
}
.clone()。虽然多占一点内存,但能避免一堆诡异的bug。尤其是多线程环境下,共享数据出问题很难排查。
2.6 本章小结
这一章的内容,说白了就是图像处理的「四件套」:读、看、改、存。再加上ROI提取和像素操作,你已经能完成YOLO项目里80%的图像预处理工作了。
嗯,这里要注意:这些基础操作一定要练到肌肉记忆。我刚开始做YOLO项目时,经常在 imread 路径和 at<T> 的坐标顺序上翻车。多写几遍,自然就熟了。