
1. 准备工作先把OpenCV环境搞定说实话OpenCV入门最大的门槛往往不是代码本身而是环境安装。我见过太多初学者卡在import cv2这一步明明按照教程装完了一运行就报ModuleNotFoundError心态直接崩掉。这篇文章作为OpenCV基础系列的第一篇我会把读取、显示、写入图像这三个最基础的操作讲透同时把安装环节容易踩的坑一并说清楚。读完这篇文章你应该能独立完成一张图片的读取、窗口展示、保存到本地并且知道每个函数背后的参数到底是什么意思。先说环境。OpenCV目前最主流的Python版本直接pip安装就行。但这里有个需要注意的点OpenCV的Python包其实分两个一个是opencv-python一个是opencv-contrib-python。前者只包含核心模块后者额外包含了一些扩展模块比如SIFT、SURF这些经典特征检测算法。如果你只是入门学习安装opencv-python就够了如果后面要做到特征匹配、目标识别建议直接上opencv-contrib-python省得以后还要卸载重装。安装命令非常简单pip install opencv-python如果网络状况不太好用国内镜像源会快很多pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后在Python环境里验证一下import cv2 print(cv2.__version__)如果输出类似4.10.0这样的版本号说明安装成功。如果报错大概率是pip版本太旧或者Python版本不匹配。OpenCV目前对Python 3.7以上的版本支持都很好建议别用太老的Python环境。另外很多人在Anaconda里装OpenCV会遇到一个问题在Anaconda Prompt里明明装好了但是在PyCharm里运行却报错找不到cv2。这个问题的根源在于PyCharm默认用的解释器不是Anaconda自带的那个Python。解决办法很简单在PyCharm的File - Settings - Project - Python Interpreter里把解释器切换成Anaconda的python.exe或者直接在里面点加号搜索opencv-python安装。我自己一直习惯用Anaconda管理Python环境然后再让PyCharm指向这个环境这样虚拟环境隔离得干净不同项目之间不会互相污染依赖版本。提示如果你是Windows用户安装过程中如果出现error: Microsoft Visual C 14.0 is required之类的报错说明你缺少C编译工具。这种情况直接去微软官网下载安装Visual Studio Build Tools选上“使用C的桌面开发”工作负载装完重试就好了。2. 第一个程序读取并显示一张图像2.1 imread读取图像的基本用法环境搞定之后我们来写第一个完整的程序。先准备一张图片放在和Python脚本同一个目录下然后执行下面的代码import cv2 img cv2.imread(test.jpg) print(img.shape)这里有几个关键点需要解释清楚。cv2.imread()返回的是什么是一个NumPy数组。OpenCV在Python底层就是用NumPy数组来表示图像的这个设计非常巧妙因为NumPy强大的矩阵运算能力天然适合图像处理。一张彩色图像会被表示成一个三维数组维度是(高度, 宽度, 通道数)。比如一张1920x1080的彩色图片打印出来的shape就是(1080, 1920, 3)这里的3代表BGR三个颜色通道。这里必须强调一个所有OpenCV初学者都会遇到的坑OpenCV读取彩色图像时通道顺序是BGR不是RGB。我们平时用的RGB顺序是红绿蓝但OpenCV历史原因沿用了BGR顺序。当你用cv2.imread()读进一张图片然后用其他库比如matplotlib的plt.imshow()直接显示会发现图片的红色和蓝色通道颠倒整体颜色发蓝发红非常诡异。解决办法要么用cv2.cvtColor()转换颜色空间要么在显示前手动交换通道。imread的第二个参数也值得说一下。默认不传的话等效于cv2.IMREAD_COLOR也就是把图像读取为BGR三通道彩色图即使原图是灰度图也会被强制转成三通道。如果你明确知道要处理灰度图可以传cv2.IMREAD_GRAYSCALE读进来直接是单通道能省一部分内存而且后续处理更快。还有一个cv2.IMREAD_UNCHANGED会保留原图的所有通道包括Alpha透明度信息处理PNG等带透明通道的图片时要用这个。很多人问为什么img.shape打印出来第一个数字是高度而不是宽度这里面涉及图像坐标系的约定。OpenCV的图像坐标系原点在左上角x轴向右y轴向下。所以数组的行数对应y方向的高度列数对应x方向的宽度。我用个图来帮助记忆想象一张表格行数就是高度列数就是宽度。这个约定和你用终端打印一个矩阵完全一致习惯了就好。2.2 imshow显示图像与窗口管理读取到图像之后自然要在屏幕上看看效果。用cv2.imshow()和cv2.waitKey()配对使用import cv2 img cv2.imread(test.jpg) cv2.imshow(Window Title, img) cv2.waitKey(0) cv2.destroyAllWindows()这段代码的运行逻辑是这样的imshow负责创建窗口并显示图像第一个参数是窗口标题第二个参数是图像数据。waitKey(0)让程序停在原地等待键盘输入参数是等待的毫秒数0表示无限期等待。destroyAllWindows()关闭所有创建的窗口。初学的时候经常有人问为什么imshow之后必须跟着waitKey如果不写waitKey窗口会一闪而过你根本看不到图像甚至程序直接崩溃。这里面的原理和高层GUI事件循环有关OpenCV的imshow只是把图像数据交给窗口系统真正把图像绘制到屏幕上、处理窗口消息事件都需要在高层的GUI事件循环里执行。waitKey的作用就是启动这个事件循环同时负责监听键盘输入。你可以理解为imshow是派活waitKey是让系统把活干完。关于waitKey的返回值也值得多说一句。用户按键时返回对应按键的ASCII码超时则返回-1。所以你可以用它来判断用户按了哪个键key cv2.waitKey(0) 0xFF if key ord(q): print(你按了q键退出程序)这里用了 0xFF是因为在某些平台上waitKey返回的可能是16位的值取低8位能保证ASCII码的正确性。这个写法在OpenCV官方例程里非常常见建议直接记住。窗口还有个很实用的函数cv2.namedWindow()可以在显示前先创建窗口。它的第二个参数可以设置窗口属性比如cv2.WINDOW_NORMAL允许用户手动调整窗口大小cv2.WINDOW_AUTOSIZE会根据图像尺寸自动调整窗口大小并且不能缩放。默认是WINDOW_AUTOSIZE。如果你要显示一张很大的图比如卫星图、UI设计稿建议用WINDOW_NORMAL不然窗口会超出屏幕边界显示不完整import cv2 img cv2.imread(large_image.jpg) cv2.namedWindow(img, cv2.WINDOW_NORMAL) cv2.imshow(img, img) cv2.waitKey(0) cv2.destroyAllWindows()2.3 waitKey参数为0和空参数的区别这里我要单独拎出来说因为这个细节在搜索热词里出现频率极高opencv waitkey为啥没参数时会卡住。很多人写cv2.waitKey()不传参数然后发现程序卡死了窗口点了没反应关也关不掉。首先要明确cv2.waitKey()不带参数等价于cv2.waitKey(0)也就是无限期等待。这其实是设计如此不算bug但对初学者来说看起来就像是卡死了。我一开始学的时候也遇到过这个困惑以为是程序出了问题后来翻官方文档才明白这个机制。那什么时候用一个具体的毫秒数呢有个典型场景实时视频流处理。每读取一帧画面就imshow一次然后waitKey(1)等待1毫秒这样既能刷新画面又不会卡死主循环。如果写waitKey(0)视频流就永远只显示第一帧了。另外一个使用频率很高的场景是waitKey(30)搭配if key 27: break用来实现按ESC键退出程序。27是ESC键的ASCII码。这个组合在摄像头采集的例程里几乎成了固定套路import cv2 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break cv2.imshow(camera, frame) if cv2.waitKey(30) 0xFF 27: break cap.release() cv2.destroyAllWindows()这个例子顺带演示了waitKey返回值的典型用法判断按键实现交互式退出。很多摄像头例程都是这个结构理解waitKey的机制之后再看这类代码就完全通透了。3. 图像写入imwrite与文件格式细节3.1 imwrite的基本调用方式读取、显示都搞定后第三个核心操作是把处理结果保存到本地文件。OpenCV里用cv2.imwrite()import cv2 img cv2.imread(test.jpg) cv2.imwrite(output.png, img)就是这么简单。第一个参数是保存的文件名第二个参数是图像数据。OpenCV会根据文件扩展名自动判断编码格式所以扩展名一定要写对写成.png就存成PNG写成.jpg就存成JPEG。如果你细心一点会注意到文件名里带了个路径output.png。这里要郑重提醒imwrite写入文件的路径中绝对不能出现中文这是OpenCV底层C函数的限制它在接收文件路径时使用的是C风格字符串对中文路径的支持非常差。一旦路径里有中文大概率会保存失败函数返回False但并不会报错提示你这个非常坑。我建议所有OpenCV相关项目涉及的图片路径一律使用英文彻底规避这个问题。那imwrite怎么判断是否保存成功呢看返回值。它成功返回True失败返回False。所以稳妥的写法是这样import cv2 img cv2.imread(test.jpg) success cv2.imwrite(output.png, img) if success: print(保存成功) else: print(保存失败请检查路径和格式)3.2 常见格式的质量参数PNG与JPEGimwrite还有第三个参数专门用来控制保存质量。这个参数对JPEG和PNG格式尤其重要。先看JPEG。JPEG是一种有损压缩格式质量参数范围是0到100默认95数值越高画质越好但文件体积也越大。如果你要控制输出文件大小可以这样写import cv2 img cv2.imread(test.jpg) cv2.imwrite(output_high_quality.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, 100]) cv2.imwrite(output_high_compress.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, 30])我实测下来质量参数设为90以上时肉眼基本看不出区别而设为30的时候图片会产生明显的压缩伪影色块感很强。所以如果你的应用场景是保存处理后的图片用于后续人工检查和审批建议质量参数设90以上如果只是存个缩略图可以适当调低。再看PNG。PNG是无损压缩格式质量参数范围是0到9代表压缩级别9表示最高压缩比。注意这个参数控制的是压缩级别不是画质PNG在压缩过程中不会丢失图像信息import cv2 img cv2.imread(test.jpg) cv2.imwrite(output_compressed.png, img, [cv2.IMWRITE_PNG_COMPRESSION, 9])压缩级别越高保存速度越慢文件相对越小。对于大多数应用默认级别完全够用不建议盲目调高因为CPU开销和文件体积的收益并不成比例。还有一个人用得比较少的参数cv2.IMWRITE_JPEG_OPTIMIZE置为1可以优化JPEG的编码在相同质量参数下略微减小文件体积。如果你的项目对存储空间敏感可以试试这个选项。这里我顺便给一个建议如果做图像处理的中间环节最好保存为PNG无损格式不要用JPEG反复保存。JPEG是损压缩每次保存都会丢一部分细节反复保存几次图片质量就会下降得非常明显。我做图像标注项目的时候就吃过这个亏数据中转过程中用JPEG保存了几轮结果后续算法精度肉眼可见地下降了。无损格式虽然文件大一些但保证信息不丢失值这个空间成本。3.3 图像格式转换与批量处理既然聊到了imwrite我顺手扩展一个非常实用的场景批量格式转换。假如你有一个文件夹里全是.bmp位图想统一转成.jpg节省存储空间用OpenCV只需要几行代码import cv2 import os input_dir input_images output_dir output_images os.makedirs(output_dir, exist_okTrue) for filename in os.listdir(input_dir): if filename.lower().endswith(.bmp): img cv2.imread(os.path.join(input_dir, filename)) if img is None: print(f读取失败: {filename}) continue new_filename filename.replace(.bmp, .jpg) cv2.imwrite(os.path.join(output_dir, new_filename), img, [cv2.IMWRITE_JPEG_QUALITY, 92]) print(f已转换: {filename})这个脚本里有个很重要的细节os.makedirs(output_dir, exist_okTrue)会在输出目录不存在时自动创建exist_okTrue表示如果目录已存在也不会报错。如果省略这行程序会因为目录不存在而直接崩溃。另一个细节是if img is None这个判断。cv2.imread在读取失败时不会抛异常而是返回None。所以如果你不检查img is None就直接操作后面cv2.imwrite会直接报AttributeError。批量处理任何文件时这个检查都是必须的。这在文件较多、可能存在损坏图片时尤其重要。4. 常见报错与排查技巧下面把初学阶段最常见的报错和问题整理成一个速查表这些几乎都是我实际教学中反复看到的问题。报错信息错误原因解决办法ModuleNotFoundError: No module named cv2OpenCV未安装或环境不对确认当前Python解释器环境重新执行pip install opencv-pythonimg is None导致AttributeError图片路径错误或文件不存在检查路径用os.path.exists()确认文件存在图片颜色不正常发蓝发紫BGR和RGB通道混淆用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换窗口一闪而过缺少waitKeyimshow后必须加cv2.waitKey(0)保存的文件无法打开或0字节路径含中文或权限不足路径改为纯英文或换一个可写目录OpenCV窗口无响应程序卡在某个耗时操作上确认主循环里是否漏了waitKey让GUI事件处理WaitKey不传参窗口假死不传参数等价于无限等待这是正常行为需要特定时间就用waitKey(50)4.1 中文路径与文件扩展名问题中文路径这个问题我前面已经提到了这里单独展开说说具体表现和规避方案。有些环境下调用了中文路径的imread会直接返回None有些环境下能读出来但imwrite保存失败还有的情况下连imread带imwrite都正常但换台电脑就不行了。这个行为在不同操作系统、不同OpenCV版本下表现不一致非常不稳定。规避方案很简单粗暴项目文件夹从上到下全部用英文命名。目录名、文件名、子文件夹都别用中文。如果接手了历史项目已经用了中文路径可以先把图片复制到一个英文临时目录处理完再拷贝回去。处理中文路径还有一个偏方用cv2.imdecode配合np.fromfile绕过imwrite的路径限制但这个手法比较复杂基础阶段先记着这个坑就好。另外说个和文件扩展名相关的注意点imwrite判断格式只看扩展名不看内部实际编码。你把一张PNG图片保存成output.jpgOpenCV会真的按JPEG方式去编码写入。这个特性用好了是方便但也会带来隐患如果你写错了扩展名比如想存PNG却写了.jpg图像会被JPEG有损压缩细节损失可能无法察觉但确实已经发生了。所以保存的时候一定要确认扩展名写对了。4.2 显示窗口关闭再打开的问题我遇到过一批同学在自己循环处理图片的时候每次都执行cv2.destroyAllWindows()然后cv2.imshow()偶尔窗口会报错崩溃或者出现“窗口已创建但图像没刷新”的诡异现象。这个问题的根源在于直接调用destroyAllWindows()再立刻调用imshow在底层GUI事件循环里会产生竞态。破局的办法是使用cv2.namedWindow()提前创建窗口配合cv2.setWindowTitle()修改标题然后用cv2.imshow()反复刷新内容最后程序结束时统一销毁窗口import cv2 cv2.namedWindow(preview, cv2.WINDOW_NORMAL) for i in range(5): img cv2.imread(fframe_{i}.jpg) if img is None: continue cv2.imshow(preview, img) cv2.waitKey(500) cv2.destroyAllWindows()这样用起来会稳很多。4.3 理解assert img is not None的防御式写法OpenCV官方文档里有这么一个惯例写法很多例程开头会有这么一行import cv2 img cv2.imread(test.jpg) assert img is not None, 图片读取失败请检查路径assert的意思是断言如果条件不满足就抛出异常后面的字符串是自定义的错误提示。这样做的价值在于一旦图片路径写错或者文件被移动程序会在第一时间报出一个明确的中文提示而不是在后面的处理中突然报一个难以追踪的AttributeError。我在写所有图像处理的脚本时都会加这个断言看起来多写了一行但调试效率提升非常明显。自己在练习的时候也可以直接用一个在线图片烧录到本地或者用OpenCV自带生成图形的方式替代比如用numpy cv2.rectangle造一张带图形的图用来测试读写流程import cv2 import numpy as np img np.zeros((480, 640, 3), dtypenp.uint8) cv2.rectangle(img, (100, 100), (400, 300), (0, 255, 0), -1) cv2.imshow(test, img) cv2.waitKey(0) cv2.destroyAllWindows()这段代码创建了一幅纯黑背景画了一个绿色填充矩形然后显示出来。对于刚开始练习读、写、显示流程的同学来说用这种方式生成测试图既不依赖外部资源又能直观验证自己的代码逻辑。5. 后续还能做什么从基础到实战基本读写操作掌握之后你对OpenCV的整体运作方式应该有了直观的感受。其实读取、显示、写入这三个操作是所有图像处理应用的地基。比如人脸检测先imread读取一张照片然后用CascadeClassifier检测人脸区域最后把框选好的人脸图imwrite保存到本地。颜色识别读取图像后用cvtColor转到HSV空间用inRange提取指定颜色区域再保存结果。这些都是把基础操作组合起来实现的。我建议你在学完这篇基础教程后立刻动手做这样一个小练习写一个简单的图片批处理工具能从一个文件夹批量读取图片统一缩放到指定尺寸输出到另一个文件夹。这个练习至少用到了文件遍历、读取、图像尺寸调整cv2.resize、写入这4个核心操作。把这个练下来你对OpenCV的IO接口会掌握得比较扎实。我自己最初学OpenCV的时候连一个简单的图片读取都折腾了很久主要卡在环境配置和中文路径这两个问题上。现在回看这些问题其实都很容易解决只是当时缺少一个系统性的梳理。这个系列后续会继续深入讲解颜色空间转换、图像滤波、边缘检测等内容但无论如何读取、显示、写入这三个基本功是会贯穿整个OpenCV学习生涯的根。把根扎稳了后面的学习才会顺畅。最后再说一个小技巧当你用cv2.imread读取图片遇到问题可以用cv2.samples.findFile(lena.jpg)来找一张OpenCV自带的示例图片这个函数会在OpenCV安装目录的样例文件夹里查找图片定位问题是不是出在自己的图片文件上。不过在较新版本里有些示例文件名称和路径有变化还是建议自己准备好一张测试图放在脚本目录里。