资讯动态

OpenCV入门教程:图像数组与像素操作基础语法

发布时间:2026/10/21 7:06:26 来源:尧图企业网站定制
这套内容是我给计算机视觉入门系列写的第二篇紧接上一篇的读图三行命令往下走。之所以把这一篇叫“语法课”是因为我的教学逻辑很简单先不讲任何算法先把图像在代码里长什么样、最基本的操作怎么执行搞清楚。你如果能把图像的像素级操作练熟后面学滤波、边缘检测、特征匹配时就不会觉得API之间是孤立的。1. 动手前的准备工作1.1 环境检查与图像准备这一篇默认你已经装好了opencv-python和numpy。如果还没装终端里执行pip install opencv-python numpy装完后建议花十秒钟做一件事进入Python环境执行下面这段代码import cv2 import numpy as np print(cv2.__version__)能正常打印版本号就行。我见过不少初学者卡在这里其实问题多半是装了opencv-contrib-python但没装主包或者两个包混装导致冲突。遇到这种问题统一卸载后只装opencv-python即可pip uninstall opencv-python opencv-contrib-python pip install opencv-python除了环境本身建议你自己准备好2~3张测试图片。不要用太大的图宽度和高度控制在几百像素左右就好方便你观察像素变化。我习惯用一张纯色图加一张带复杂纹理的照片来测试这样既能验证色彩操作的规律也能看效果是否明显。1.2 从读图到写图的“标准三连”上一篇讲过imread和imshow这里再用一个最小例子把整个流程串起来因为后面大量操作都会依赖这个模板import cv2 img cv2.imread(test.jpg) print(img.shape) # (高, 宽, 通道数) cv2.imshow(window, img) cv2.waitKey(0) cv2.destroyAllWindows()waitKey(0)的意思是无限等待键盘输入不写这一句窗口通常一闪而过或者显示为纯灰色。destroyAllWindows()负责关闭所有OpenCV创建的窗口。再看写图cv2.imwrite(output.jpg, img)写图时要注意一点imwrite的成功与否不会用报错告诉你。它返回布尔值True表示保存成功。特别是在路径写错、目录不存在时它往往只是静默返回False。判断是否成功建议写成success cv2.imwrite(output.jpg, img) if not success: print(保存失败检查路径)2. 图像在 OpenCV 里的真正形态2.1 三维数组高、宽、通道把一张彩色图读进来以后它到底是什么答案是一个三维的NumPy数组。img.shape返回的三元组依次是高度行数、宽度列数、通道数。OpenCV读取的彩色图是BGR顺序三个通道分别代表蓝、绿、红而不是常见的RGB。这里有一个初学者很容易懵的点你在plt.imshow或某些工具里看到的红蓝颠倒往往就是BGR和RGB顺序惹的祸。如果你用cv2.imread读图再交给matplotlib显示就必须先转换通道顺序import cv2 import matplotlib.pyplot as plt img_bgr cv2.imread(test.jpg) img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) plt.imshow(img_rgb) plt.show()为什么会这样设计历史原因是早期显示器和图像格式的底层约定与计算机视觉处理习惯不同。既然OpenCV官方文档和函数都默认BGR你在写代码时就保持一致先不要问“为什么不是RGB”遇到跨库显示时再转换即可。灰度图和二值图就简单多了它们通常是二维数组形状只有高和宽没有第三维。理解这一点后面做阈值、找轮廓时就不会被数组维度弄晕。2.2 像素的读取与修改最直接的像素读取方式是用下标索引b, g, r img[100, 200]这里100是行号y坐标200是列号x坐标。也就是说第一个索引表示纵坐标第二个索引表示横坐标。无数新手在第一步就被这个坐标顺序坑过因为平时我们习惯先说x再说y。如果你只想读某个通道的像素值可以这样b img[100, 200, 0] # 第0通道也就是蓝色通道 g img[100, 200, 1] # 第1通道绿色通道 r img[100, 200, 2] # 第2通道红色通道修改像素同理img[100, 200] (255, 255, 255) # 把该像素变成白色一次修改一个像素看起来没什么用但它是理解滑动窗口、区域填充和各种图像处理算法的基础。你要养成把图像当数组看待的习惯而不是当“图片”看待。数组上的逻辑就是图像上的逻辑。3. 遍历与裁剪操作图像的基本功3.1 像素遍历的速度陷阱既然图像是数组很多人的第一反应是用双重for循环遍历每个像素。比如想把一张图整体变亮一点height, width img.shape[:2] for y in range(height): for x in range(width): img[y, x] img[y, x] 30这个逻辑没错但你会立刻发现它慢得离谱。一张1000×1000的图双重循环是100万次像素操作Python的for循环性能根本扛不住。正确的思路是向量化操作直接用NumPy对整个数组做加减运算。img2 img 30这一行代码能瞬间完成同样的效果因为NumPy底层用的是C语言级别的批量计算。如果你需要针对某些区域做操作也可以利用切片和掩码。总之在OpenCV里写Python代码要时刻记得“能批量就别循环”。循环的唯一合理场景是像素数量很小比如只有几个点需要处理。3.2 区域裁剪的正确姿势裁剪图像是典型的高频操作。因为图像是数组裁剪本质上就是切片crop img[100:300, 200:400]这里得到的是从行100到299、列200到399的一个矩阵。再说一遍前后两个切片范围第一个是高度方向第二个是宽度方向。裁剪得到的区域其实是原图的一个视图它们在内存里可以共享数据。如果你修改了crop的值原图对应区域也可能被修改。想要复制区域后续若要对小块图像单独处理而不影响原图可以用.copy()crop img[100:300, 200:400].copy()关于裁剪我再提一个应用场景很多时候你想截取图像里的某个ROI感兴趣区域去做识别或处理比如识别车牌、检测人脸都是这样先裁出小区域再送进模型。把裁剪练熟你后面做目标检测的后处理时会顺手非常多。3.3 缩放与尺寸变换缩放在OpenCV里用resize实现resized cv2.resize(img, (new_width, new_height))注意第二个参数是(宽, 高)输出尺寸的长宽和shape里的顺序是反的。如果你不记得我可以教你一个不容易错的办法h, w img.shape[:2] resized cv2.resize(img, (w // 2, h // 2))这是把宽高各缩小一半。在实际项目中经常需要按比例缩放而不是硬编码成固定尺寸。比如我要把宽度限制在800以内高度等比缩放h, w img.shape[:2] target_width 800 scale target_width / w target_height int(h * scale) resized cv2.resize(img, (target_width, target_height))缩放插值方式可以通过interpolation参数指定。常用的有INTER_NEAREST最近邻、INTER_LINEAR双线性、INTER_AREA区域插值。缩小图片时用INTER_AREA更平滑放大图片时用INTER_LINEAR或INTER_CUBIC效果更好。这个细节在你的输出画质要求比较高时非常关键。4. 几何变换旋转、翻转与仿射4.1 旋转与翻转的直观理解翻转操作很简单适合作为几何变换的入门flip_h cv2.flip(img, 1) # 水平翻转像照镜子 flip_v cv2.flip(img, 0) # 垂直翻转上下颠倒 flip_both cv2.flip(img, -1) # 同时水平和垂直翻转旋转稍微绕一点。OpenCV有一个专用函数cv2.rotate它只能做90度的整数倍旋转rotated_90 cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)但如果你要旋转任意角度比如把图像顺时针旋转45度就必须用getRotationMatrix2D和warpAffine配合。前者生成一个2×3的仿射变换矩阵后者把这个矩阵应用在图像上。h, w img.shape[:2] center (w // 2, h // 2) matrix cv2.getRotationMatrix2D(center, 45, 1.0) rotated cv2.warpAffine(img, matrix, (w, h))这里45是逆时针角度如果想顺时针传负值即可。warpAffine这个词看起来吓人其实含义很简单“对图像做仿射变换”。仿射变换能保持直线和平行线不变所以旋转、平移、缩放都属于仿射变换。后面的透视变换也是类似的概念先把这些名词混个脸熟往后看文档就不慌。4.2 仿射变换与透视变换的区别个人建议你在入门阶段至少亲手做一次仿射变换和透视变换的代码实验因为几乎所有矫正类任务都会碰到它们。仿射变换的基本做法是在原图中取3个点在目标图中对应3个点OpenCV根据这3对点推导出整个变换矩阵。src_pts np.float32([[50, 50], [200, 50], [50, 200]]) dst_pts np.float32([[0, 100], [200, 50], [100, 250]]) matrix cv2.getAffineTransform(src_pts, dst_pts) result cv2.warpAffine(img, matrix, (w, h))透视变换则需要4个点。典型的应用是拍了一张倾斜的书页或招牌想把视角拉正src_pts np.float32([[10, 20], [300, 20], [10, 380], [300, 380]]) dst_pts np.float32([[0, 0], [w, 0], [0, h], [w, h]]) matrix cv2.getPerspectiveTransform(src_pts, dst_pts) result cv2.warpPerspective(img, matrix, (w, h))这个技术最大的难点不在函数本身而在于如何自动找到那4个点。入门阶段你可以手动指定坐标观察变换效果后面学完边缘检测和凸包检测再想办法自动提取角点。总之先把能手动控制的全流程跑通。4.3 平移操作的底层逻辑平移也是几何变换的一种。用warpAffine实现平移的方法是构造这样一个矩阵matrix np.float32([[1, 0, 100], [0, 1, 50]]) shifted cv2.warpAffine(img, matrix, (w, h))这里[[1, 0, 100], [0, 1, 50]]表示x方向平移100像素y方向平移50像素。右侧出现的空白区域默认是黑色如果想要白色或其他颜色可以通过borderValue参数指定。为什么非要搞一个矩阵出来而不是直接提供shift函数因为矩阵乘法把平移、旋转、缩放统一在同一套数学框架里。当你想连续做多个操作时可以先在数学层面把多个矩阵合成为一个矩阵最后只对图像做一次变换。这是很多图像处理框架的共同设计理念值得理解。5. 绘图与文字标注5.1 画线、矩形、圆与多边形在图像上画图是调试和结果可视化的利器。比如你要验证目标检测框画得准不准就需要在图上画矩形。画一条线cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)(x1, y1)是起点(x2, y2)是终点颜色是BGR三元组2是线宽。注意这里的坐标形式和img[y, x]是反的绘图接口接收的是(x, y)数组索引接收的是(y, x)。这是新手最容易混的坑之一。画矩形也很接近cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)其中(x1, y1)是左上角(x2, y2)是右下角。如果最后一个参数改为-1就是填充整个矩形。画圆cv2.circle(img, (cx, cy), radius, (255, 0, 0), -1)这里(cx, cy)是圆心坐标radius是半径-1表示填充。画多边形稍微复杂一点需要先构造顶点数组数据类型必须是非负的整数类型通常用int32pts np.array([[100, 100], [200, 100], [150, 200]], np.int32) cv2.polylines(img, [pts], True, (255, 255, 0), 2)5.2 在图像上写中文的注意点OpenCV自带的putText只支持英文和数字不支持中文。如果你直接传入中文字符串可能会显示为乱码或方框。cv2.putText(img, Hello, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)中文显示通常有几种替代思路一种是用PIL绘制中文后再转回OpenCV图像格式另一种是把中文渲染成带透明通道的图片再通过图像融合贴上去。无论哪种都比putText多几步操作所以很多实际项目标注中文时会直接用其他库处理。我建议初学者先用英文标注跑通流程等后面需要做数据可视化或界面标注时再解决中文显示问题不要一开始就被这个细节打断学习节奏。6. 颜色空间与阈值操作6.1 BGR、灰度与HSV转换颜色空间转换是OpenCV里运用极其频繁的一类操作。最基础的两个转换gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)为什么要转灰度很多高级算法边缘检测、角点检测、轮廓提取的输入端会直接要求灰度图。灰度图只有一个通道数据量小、干扰少算法处理起来也快。为什么要转HSV因为HSV把色调、饱和度、亮度分开存放做颜色筛选时非常直观。比如要筛选红色区域在BGR里可能需要同时判断3个通道的数值范围麻烦而在HSV里只要判断H通道落在某个区间即可下面这篇文章里筛选彩色物体的用法将来你会经常遇到。6.2 二值化操作与阈值筛选二值化是图像分割的地基。简单阈值处理长这样_, binary cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)这行代码的意思是灰度值大于127的像素设为255白色小于等于127的设为0黑色。THRESH_BINARY是二值化类型也可以换成THRESH_BINARY_INV进行反转。不过固定阈值最大的问题是光照不均时效果很差。实际项目中更推荐自适应阈值或大津法OTSU。大津法的用法很简单只需要额外加一个标志位_, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)它会自动寻找一个能把前景和背景分得最开的阈值不用你手工指定127这个数。这个技巧在扫描文档、处理文字图像时非常实用。阈值操作和颜色范围筛选经常配套使用。比如我只想保留画面里的红色物体一种做法是lower (0, 100, 100) upper (10, 255, 255) mask cv2.inRange(hsv, lower, upper) result cv2.bitwise_and(img, img, maskmask)inRange会生成一个掩码图白色区域表示符合颜色范围的像素黑色区域表示剔除掉的像素。bitwise_and配合掩码就能把原图中符合范围的区域保留下来其余变成黑色。到这里你已经“无意识地”接触到了图像运算的核心操作下一节我专门展开它。7. 图像运算加法、加权与位操作7.1 图像加法与加权融合图像加法不是数字上的直接相加因为它有像素值上限0~255超过上限就自动截断或取模。img_add cv2.add(img1, img2)cv2.add做的是饱和运算超过255就变成255不会循环回绕。而直接用img1 img2做NumPy加法超过255会回绕出现色彩反转。加权融合更适合做透明叠加效果blended cv2.addWeighted(img1, 0.7, img2, 0.3, 0)意思就是0.7 * img1 0.3 * img2 0前两个图的权重之和不需要是1但一般为了效果自然会让它们加起来等于1。这种技术在给图像加水印、叠加半透明蒙版时经常用到。7.2 位运算实现ROI与蒙版位运算在OpenCV里是一个被很多人低估的功能。四个基本函数bitwise_and、bitwise_or、bitwise_xor、bitwise_not。bitwise_and最常见的用法就是配合掩码提取ROI。我已经在上面的颜色筛选里见过一次了mask np.zeros(img.shape[:2], dtypenp.uint8) mask[100:300, 200:400] 255 result cv2.bitwise_and(img, img, maskmask)这段代码把图像中[100:300, 200:400]范围外的所有像素置为纯黑区域内保留原色彩。这和直接裁剪的效果不同裁剪会改变图像尺寸而位运算保持原尺寸方便后续与整图的配准。bitwise_not可以用来做二值图反色有时比改变阈值类型更直观。在位运算的基础上结合阈值操作和轮廓检测你可以处理很多现实问题把图片中的某个色块单独抠出来、统计某个物体的面积、甚至做一个简单的变装效果。7.3 图像拼接与np.hstack最后补充一个非常提升效率的小操作多张图并列显示。combined np.hstack([img1, img2]) cv2.imshow(side_by_side, combined)np.hstack要求两张图高度一致np.vstack要求宽度一致。如果你想把不同尺寸的图并列展示先统一用resize搞定。这个技巧在你做算法对比、参数调试时能省不少时间。8. 新手最容易踩的坑坐标、类型与报错8.1 坐标顺序混乱我在前文反复强调过数组索引是(y, x)绘图接口是(x, y)resize的尺寸参数是(宽, 高)shape返回的是(高, 宽)。这些顺序如果不统一记新手必踩坑。我的习惯是凡是拿到一组坐标和尺寸先问自己一句——“这到底是x在前还是y在前是不是宽在前”多问几次甚至可以在代码注释里特别标注。我发现写上几周注释之后这个混淆问题基本就消失了。8.2 数据类型不匹配OpenCV里的图像数组通常是uint8类型。但很多运算在中间过程会产生浮点数比如归一化之后的值、仿射变换的坐标等。如果忘记用.astype(np.uint8)转换后面再调用某些绘图或写图函数可能报出类型不匹配的错误或者显示一片花图。具体来说当你做图像融合、像素级运算、模型输入预处理时要格外关注数组的dtype。一个小建议在关键步骤前后打印img.dtype用几秒钟看一眼能避免大量讨厌的显示问题。8.3 常见报错速查报错信息 / 现象大概率原因解决办法cv2.error: ... assertion failed图像为空通常是路径写错或imread读入失败打印img is None?检查文件路径显示窗口全灰或一闪而过缺少waitKey或waitKey参数设成0仍被CPU抢占确保waitKey(0)和destroyAllWindows执行shape出现(h, w, 3)但你想用(w, h)去循环行列顺序记反统一用h, w img.shape[:2]保存的图片是黑色很可能图像数组全部变成0说明前面的掩码或阈值逻辑取反了检查img.min()和img.max()matplotlib显示颜色不对BGR没转RGBcv2.cvtColor(img, cv2.COLOR_BGR2RGB)np.hstack报维度不一致两张图高度或通道数不相同先resize统一尺寸再拼接8.4 常见问题与排查技巧实录我在实际教学里被问得最多的一个问题是为什么我用cv2.imread读进来用plt.imshow显示画面看起来发蓝这个问题的根因就是BGR与RGB的顺序。如果你干活用的是OpenCV整套流程——读图、处理、写图全程保持BGR不变其实什么问题都没有。只有在跨库显示时才需要转换这点我推荐你在脑子根深蒂固地记住。还有一个非常经典的问题图像处理后无法保存。通常表现为imwrite返回False。常见原因是路径里的目录不存在Python不会自动创建目录。解决办法是先import os确保目录存在再保存import os os.makedirs(output_folder, exist_okTrue) success cv2.imwrite(output_folder/result.jpg, img)另一个我特别想提醒的点处理前看一眼img.shape。很多同学代码明明没错但处理结果和预期完全不符追到根上才发现图像加载的是灰度图只有2维却按彩色图的3通道去操作。打印shape花不了几秒钟但可以省掉半小时的困惑。几句实在话每次带新人我都觉得最关键的并不是背住多少个API而是建立“图像就是数组”这个心智模型。你一旦接受了这个概念再去看色彩空间、滤波器、卷积、边缘检测全部都是在数组上做数学运算只是OpenCV帮你封装好了高效实现。这一篇的内容如果全部亲手敲一遍大概需要一两个小时。强烈不建议只是复制粘贴代码看完就划走因为OpenCV的很多坑真的要等你出错一次才会长记性。我见过太多人收藏了无数教程最后遇到最基础的报错还是两眼一抹黑。下一篇我会从这个基础出发带你上手高斯模糊、Canny边缘检测和轮廓提取这三板斧能帮你完成很多有趣的视觉小项目。只要你现在把像素、坐标、通道、颜色转换这几个概念彻底搞懂后面只会越学越顺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑