005、使用OpenCV读取、显示与保存图像
2026/9/18 21:30:15 网站建设 项目流程

005、使用OpenCV读取、显示与保存图像

写这篇笔记的时候,我正对着一个“读出来全是黑的”的bug发愁。客户发来的图片明明有内容,代码里imread一读,Mat的data指针非空,但imshow出来就是一块黑板。后来发现是图片路径里带了个中文目录名,而Windows下OpenCV的imread用的是ANSI编码,路径里的“测试”两个字变成了乱码,文件根本没打开成功,只是Mat对象没报错而已。这个坑我踩了好几次,所以今天干脆把OpenCV读写显示图像这几个基本操作里容易踩的雷,一次性倒出来。

先看读取。cv::imread是入门第一个函数,原型很简单,第一个参数是文件路径,第二个参数是一个flag,控制怎么读。很多人不管它,直接写imread(“xxx.jpg”),默认是IMREAD_COLOR,也就是把图片转成3通道BGR,alpha通道直接丢掉。注意是BGR不是RGB,OpenCV内存里存的就是BGR顺序,后面凡是碰到颜色分离、像素遍历、显示,都得记住这个顺序。你要是习惯用RGB思维,比如用cv::split把通道拆开,第一通道是Blue不是Red,调试颜色不对的时候别懵。另外这个flag还有个坑,IMREAD_COLOR会把灰度图强制转成三通道的BGR,每个像素的三个值相等。如果你真想要灰度图,得显式传IMREAD_GRAYSCALE,这时返回的单通道Mat。还有IMREAD_UNCHANGED,保留原图所有通道,包括alpha。一般做目标检测预处理,要么灰度,要么BGR,很少用UNCHANGED,但如果你想确认图片是不是有alpha,用这个读再检查channels()。

imread返回是Mat,读失败的时候,这个Mat的data是nullptr,rows和cols都是0。所以正确姿势是读完后立刻判断一下if(img.empty()),这是老生常谈,但真有人不写,后面用到img.rows直接崩。我自己的习惯是写一个loadImage函数,里面做判断,失败就打印日志并返回错误码,别让空Mat往下传。

再说路径。刚才说了中文目录名在老版本OpenCV会出问题,其实不只中文,任何非ASCII字符都可能翻车。解决方案有两个:一是把路径改成英文;二是用cv::imdecode配合std::ifstream读文件字节流,再解码成Mat。后者的代码大概是std::ifstream ifs(path, std::ios::binary); std::vector buf((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); cv::Mat img = cv::imdecode(buf, cv::IMREAD_COLOR); 这样能绕开路径编码问题,但前提是你得包含fstream和opencv2/imgcodecs.hpp。这个写法我建议放进你的工具函数库,指不定哪天就救你一命。

读取之后肯定要显示看一眼。cv::imshow(“窗口名”, mat)是最常用的。但imshow有个要求,Mat的类型得是8位无符号的,也就是CV_8U。你要是从摄像头拿的帧,可能是CV_8UC3没问题,但如果你自己处理成CV_32F浮点图,直接imshow会显示成一片白或者黑,因为imshow对浮点数据默认当成0-1范围映射,超出范围就截断。这时候需要先归一化到0-255并转成CV_8U,用cv::normalize或者直接convertTo。这是显示调试图时最常碰见的问题。

imshow之后必须配waitKey。不写waitKey的话,窗口一闪而过,或者干脆不显示。waitKey这个函数的机制是等待用户按键,参数是等待毫秒数。写0表示无限等待直到按键。在显示单张图片的调试场景,通常这么写cv::imshow(“test”, img); cv::waitKey(0); 但注意waitKey除了等待按键,还负责处理GUI事件,包括窗口重绘、鼠标响应、视频帧更新等等。所以如果你在一个循环里显示视频,必须让waitKey(30)这类带小延迟的调用在循环里跑,否则界面会卡死。还有一个点,waitKey在高分辨率或无桌面环境下可能不响应,比如树莓派通过VNC连接,或者远程SSH开了X11转发,有时候按键焦点不在OpenCV窗口上,按下还没反应。这时可以试试点一下窗口再按,或者用waitKeyEx获取更底层键值。

窗口管理还有个namedWindow函数,你可以先创建窗口,指定属性,比如cv::WINDOW_NORMAL表示窗口可调整大小,cv::WINDOW_AUTOSIZE默认,窗口大小跟图像一致不能拉伸。为了调试时能把大图拖小,我喜欢用WINDOW_NORMAL。另外用WINDOW_KEEPRATIO可以在拉伸时保持宽高比。这个窗口是跟OpenCV highgui模块绑定的,它的生命周期直到destroyWindow被调用或者程序退出。如果你连续imshow同一个名字,就是同一个窗口刷新。如果忘记销毁,有时会残留一个灰色窗口,尤其在Jupyter Notebook里跑的时候特别烦。

保存图像,cv::imwrite。它的坑也不少。扩展名决定了编码格式,你想保存成PNG就写"out.png",JPG就写"out.jpg"。但OpenCV不会根据扩展名猜测编码,它是按后缀映射到编码器的。如果你写了个不支持的扩展名比如".bmp2",那会返回false,而且不报错。所以保存完要检查返回值。imwrite还有第三个参数,是vector类型的编码参数。比如保存JPEG可以设置质量:std::vector params; params.push_back(cv::IMWRITE_JPEG_QUALITY); params.push_back(95); 保存PNG可以设置压缩级别:IMWRITE_PNG_COMPRESSION,0-9,默认是3。这里有个小经验,老版本OpenCV对PNG压缩级别处理有bug,设置过高的级别导致写出来的文件超大或者耗时严重,建议用默认。还有个特别容易翻车的是imwrite不支持中文路径,跟imread一个毛病。而且imwrite遇到路径中不存在目录时不会帮你建目录,直接失败,返回值还是false。我习惯在保存前用boost::filesystem或者C++17的std::filesystem创建父目录,再调用imwrite。

说到保存,还有一个常见需求是把Mat数据编码成内存缓冲,用于网络传输或数据库存储。不要用imwrite写临时文件再读,效率低。用cv::imencode(“.jpg”, mat, buf, params),buf是std::vector,直接可以写socket或者塞进数据库。对应的解码就是刚才说的imdecode。这套API很实用,项目里一旦涉及文件传输,基本都会用到。

再提醒一点通道顺序的问题。很多人把BGR当成RGB来做颜色过滤,比如用cv::inRange抠红色目标,结果发现怎么调阈值都不对。你拿到的图片如果是imread读的,就是BGR。如果你用skimage或者matplotlib读图,那是RGB。OpenCV里把RGB图像直接交给imshow显示,颜色会偏蓝红互换,人脸看起来像阿凡达。这是个经典笑话,但也是每次新人都要经历一遍的。解决方法是cvtColor(src, dst, cv::COLOR_BGR2RGB)再给别的库用。反过来,外部图像进OpenCV就COLOR_RGB2BGR。

还有Mat的浅拷贝问题。在调试时,你想把图像传给某个函数处理,写cv::Mat crop = img; 你以为这是复制了一张图,实际上crop和img共享同一个数据块。你改crop,img也变了。想要真正的深拷贝得用img.clone()或cv::Mat::copyTo。这个跟读取显示无关,但看图像数据时经常碰到,比如你想截取一个区域做分析,然后用imshow显示crop,结果原图的像素跟着变,排查半天才发现是浅拷贝。目标检测里尤其要小心,因为检测框缩放、裁剪、resize都涉及Mat数据的共享和引用。

回到开头那个中文路径问题,顺带说下调试技巧。遇到imread读不出来,先别怀疑OpenCV坏了,用fopen或者ifstream打开同一个路径,看看是否成功。如果文件流都打不开,那一定是路径的问题。再用std::cout << img.empty() << std::endl; 确认是否真的读到。这种“打印一下”的习惯比什么都管用。我在笔记本上常年放着一个debug_image函数,它接收Mat和窗口名,打印尺寸、通道数、像素均值、文件是否为空,然后imshow。每次图像处理前先跑一遍这个函数,能省下大量猜谜时间。

最后给点个人建议。读取图像用绝对路径,别用相对路径。相对路径依赖工作目录,不同IDE、不同启动方式工作目录都可能不一样,调试时看着像读取失败,实际上路径没找到。显示图像时窗口大小别用默认的AUTOSIZE,除非你的屏幕足够大,否则一张4000x3000的照片会把你整个屏幕撑满,窗口还关不掉(因为标题栏在屏幕外),特尴尬。保存图像前一定检查目录存在,图像编码格式是否支持,有没有写权限。另外,OpenCV的imwrite对多通道Mat的通道数有限制,一般只支持1、3、4通道,你要是弄了一个2通道的Mat去保存,返回值直接false。

写代码这件事,99%的时间都在跟边界条件、类型不匹配、环境差异较劲。OpenCV这几个基础函数看着简单,但每一个都藏着小陷阱。把imread、imshow、imwrite的这堆毛病摸清楚,后面的YOLO实战才能少分心去处理图像输入输出的低级错误。下一篇我们开始讲图像预处理——resize、归一化这些,那才是喂给YOLO之前真正的重头戏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询