简介OpenCV图像处理基础入门资料以PDF电子书形式整理面向计算机视觉初学者与人工智能方向的开发人员。内容围绕图像读取、显示、合并、边界填充、图像融合、阈值处理等核心操作展开附有可运行的详解代码帮助读者快速理解OpenCV的常用API与图像矩阵处理逻辑适合作为系统学习或备查的基础手册。整包共1个文件格式为PDF大小约132KB便于移动端阅读或离线收藏。目前已有1211人浏览学习受到计算机视觉入门人群的持续关注。通过学习这份资料读者可以掌握cv2.imread、cv2.imshow、cv2.merge、cv2.copyMakeBorder、cv2.addWeighted、cv2.threshold等基础函数的用法并能够在实际图像处理任务中独立完成简单的读写、拼接、填充与二值化操作为后续图像特征提取、目标检测等进阶内容打下扎实基础。1. OpenCV图像处理入门先弄清图像在内存里存的是什么拿到“计算机视觉图像处理 Opencv 基础知识附详解代码上”这类资料很多人的第一反应是赶紧找代码跑起来。但我在带 A 同学和某公司新人时发现凡是后面反复翻车的人几乎都卡在同一件事上没搞懂 OpenCV 里的图像本质上就是一个多维数组。灰度图是二维数组彩色图是三维数组三个维度分别是高、宽、通道数。你之后做的所有滤波、阈值、形态学操作本质都是对这个数组做数学运算。这篇笔记就按我平时带人的顺序来讲先建立这个数组心智模型再逐段拆解读图、滤波、边缘检测、形态学这些基础代码最后把最容易踩的坑一次性列清楚。2. 读取、显示与保存图像imread/imshow/imwrite 的最小代码2.1 一张图读进来以后到底是什么数据结构OpenCV 的 Java 版和 Python 版我都用过日常调试我用 Python 接口最多原因是能直接打印数组形状和像素值对入门者最友好。下面这段代码是每个项目我都会先跑一遍的最小示例import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) print(img.shape) print(type(img)) print(img[0, 0])第一次跑通的人通常会被输出结果吓一跳shape 打印出来是(height, width, 3)而img[0, 0]返回的像素值顺序是[蓝, 绿, 红]而不是我们直觉里的 RGB。这就是 OpenCV 的 BGR 通道顺序它继承自早期相机和显示设备的底层约定。你后面做颜色识别、图像合成时只要忘记这一条结果必然偏色。这里有个细节必须强调cv2.imread返回的数组类型是numpy.ndarray不是 OpenCV 自己定义的类。这意味着所有 numpy 的切片、广播、矩阵运算都能直接作用在图像上。我经常用img[:, :, 0]单独取出蓝色通道来观察这在调试时比调用 API 更直观。参数cv2.IMREAD_COLOR表示强制读成三通道彩色图即使原图是灰度图也会被复制成三个通道cv2.IMREAD_GRAYSCALE则相反会把彩色图压成单通道。需要注意IMREAD_UNCHANGED会保留原图的通道数和位深例如 PNG 的透明通道。当图片路径含中文时imread经常返回None这个问题后面章会单独讲。2.2 显示与保存为什么窗口里看到的和数组不一样读图只是第一步能正确显示才能确认数据没读错。显示代码固定三件套import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) cv2.imshow(window, img) key cv2.waitKey(0) if key ord(q): cv2.destroyAllWindows()cv2.imshow的第一个参数是窗口名第二个是图像数组。cv2.waitKey(0)表示无限等待键盘输入返回值是按键的 ASCII 码如果填cv2.waitKey(30)则每 30 毫秒返回一次常用于视频帧循环。cv2.destroyAllWindows()负责释放所有窗口写不写其实不影响程序退出但不写的话在 Jupyter 这类环境里容易积累僵尸窗口。显示过程中最容易让新手困惑的是图像数组里的值是 0 到 255 的整数窗口显示的却是连续色调。这是因为 OpenCV 在底层把 8 位无符号整数映射到了显示设备的色彩空间这个过程自动完成不需要你干预。保存则只有一行cv2.imwrite(output.jpg, img)保存的格式由文件扩展名决定.jpg会走有损压缩.png走无损压缩。imwrite没有返回值提示成功失败我一般会检查文件是否存在来确认写入结果。如果想控制 jpg 压缩质量需要用参数cv2.IMWRITE_JPEG_QUALITY后面章节会有涉及。2.3 彩色图转灰度图通道数变化带来的理解门槛读了图、会显示、能保存下一步最常做的就是彩色转灰度。灰度图去掉了颜色信息只保留亮度数据量从三维降成二维后续很多算法只在灰度图上定义。代码是固定的import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) print(gray.shape)转换后gray.shape变成(height, width)没有通道维度。cvtColor的第二个参数是转换码COLOR_BGR2GRAY是彩色转灰度最常用的一个。OpenCV 里这种转换码还有几十种比如COLOR_BGR2RGB用于调整通道顺序COLOR_BGR2HSV用于色调饱和度空间后两者在做颜色识别时会频繁出现。灰度化的原理是加权平均OpenCV 按 0.114 的蓝、0.587 的绿、0.299 的红权重把三通道合成单通道这个权重来自人眼对三种颜色的敏感度差异。直接取三个通道的平均值虽然也能得到灰度图但对比度会明显偏弱所以不建议自己用np.mean替代cvtColor。3. 颜色空间转换与滤波灰度化、高斯模糊和边缘检测怎么配3.1 高斯模糊为什么去噪要选它而不是均值模糊滤波是图像处理里最有“手感”的部分。我见过很多人直接把高斯模糊当成“让图变糊”的工具但它的真实用途是去除高频噪声为后续边缘检测和阈值分割做准备。import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) blurred cv2.GaussianBlur(img, (5, 5), 0)GaussianBlur的参数三个输入图、卷积核大小、标准差。卷积核(5, 5)表示在 5×5 的邻域内做加权平均核越大图像越平滑但边缘也会越模糊。标准差传 0 时OpenCV 会根据核大小自动计算一个合适的值。和均值模糊cv2.blur相比高斯模糊给中心像素更高的权重远处像素权重指数衰减。这个特性让它能在抑制噪声的同时保住更多边缘结构。从实践来看做 Canny 边缘检测之前几乎没有例外地要先做一次高斯模糊不然检测结果会被噪声点切成碎片。高斯模糊的一个容易忽略的参数是核的边长必须是奇数传(4, 4)会直接报错OpenCV 要求卷积核有明确的中心点。我在代码里固定写(5, 5)是基于多数场景的性价比选择核再大比如(9, 9)图像细节损失会明显加重对入门练习来说没必要。3.2 Canny 边缘检测两个阈值背后的取舍逻辑边缘检测是图像处理入门的第一个“成果型”操作因为把彩色照片变成白底黑线的边缘图视觉反馈非常直观。Canny 是实际项目里最常用的算法代码不出五行import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(blurred, 50, 150) cv2.imshow(edges, edges)Canny的三个核心参数输入图、低阈值、高阈值。梯度幅值高于高阈值的像素确定保留为边缘低于低阈值的直接丢弃介于两者之间的像素只有与高阈值边缘相连才保留。这就形成了一个“滞后阈值”机制能有效抑制断裂边缘。阈值怎么调是我被问得最多的问题。我的经验是低阈值取高阈值的一半到三分之一比较稳妥例如高阈值 150、低阈值 50。如果检测出的边缘线太碎说明低阈值偏高把低阈值降到 30 以下会有改善如果边缘粗得连成一片说明高阈值偏低拉到 200 以上再试。这个调节过程没什么玄学本质是噪声强度和你想要保留的细节之间的博弈。我建议新手把上面对应的参数做成可调节版本跑一遍import cv2 img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (7, 7), 0) low 40 high 120 edges cv2.Canny(blurred, low, high) merged cv2.hconcat([gray, edges]) cv2.imshow(compare, merged)cv2.hconcat把灰度图和边缘图横向拼在一张窗口里显示这种对比方式比来回切换窗口高效得多。low和high我故意设成两个变量方便你手改数值观察结果变化。Canny 的输入必须是单通道灰度图直接传 BGR 彩色图会得到错误结果运行时 OpenCV 不一定报错但输出完全不可用这一点要特别注意。3.3 锐化操作用得上吗基础阶段不建议碰很多新手做完模糊和边缘检测后会试着用filter2D做锐化想让图像看起来更清晰。我的建议是基础阶段先跳过锐化。锐化本质是原图加上高通滤波的结果实现不当会把噪声同时放大产生一圈白边这在后续处理中是很难处理的伪影。import cv2 import numpy as np kernel np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtypenp.float32) sharpened cv2.filter2D(img, -1, kernel)这段代码本身没有错filter2D第二个参数-1表示输出图和输入图同深度第三个参数是自定义卷积核。但我想提醒的是锐化会让原本轻微压缩痕迹变成肉眼可见的瑕疵。如果确实需要锐化也应该先做高斯模糊去除噪声再做锐化。从入门节奏看先把模糊、阈值、形态学练熟锐化放到后面接触频域时再理解会顺畅很多。4. 阈值分割与形态学操作把目标从背景里抠出来的实战参数4.1 二值化为什么比你想的更依赖预处理边缘检测找到了“轮廓”但真正想把目标区域从背景里分离靠的是阈值分割。最简单也最常用的方式是固定阈值二值化import cv2 gray cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE) blurred cv2.GaussianBlur(gray, (5, 5), 0) ret, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)这段代码里threshold的第二个参数传了 0同时启用cv2.THRESH_OTSU意思是让算法自动计算最佳阈值返回值ret就是计算出的那个阈值。Otsu 方法会在所有可能的阈值里选一个让前景背景类间方差最大的值对双峰直方图的图像效果非常好。需要留意的是THRESH_OTSU只能配合灰度图使用而且对光照不均匀的图像效果会变差。如果图片左边亮右边暗固定阈值很难同时兼顾两边这时要么用自适应阈值cv2.adaptiveThreshold要么先做形态学操作去除照度影响。后面章节会有相应代码。cv2.THRESH_BINARY表示大于阈值的像素置为 255小于阈值置为 0。与之对应的THRESH_BINARY_INV则是反向操作在背景比目标亮时使用。我一般会把两张结果拼在一起看确认目标和背景谁该是白色。4.2 腐蚀与膨胀先理解核再理解迭代次数二值化之后白色区域里通常有噪点目标边缘可能坑坑洼洼。形态学操作的腐蚀和膨胀就是处理这些问题的主力。腐蚀是取邻域内最小值让白色区域变小膨胀是取最大值让白色区域变大。import cv2 import numpy as np binary cv2.imread(mask.png, cv2.IMREAD_GRAYSCALE) kernel np.ones((5, 5), np.uint8) eroded cv2.erode(binary, kernel, iterations1) dilated cv2.dilate(binary, kernel, iterations1)kernel是腐蚀和膨胀的邻域形状np.ones((5, 5), np.uint8)生成一个 5×5 的矩形核。iterations是连续操作的次数每多一次就多腐蚀或膨胀一层。实际上一次操作和多次操作的效果并不完全等价但迭代次数少时差异不明显。腐蚀后白色区域变小能去掉小块的白色噪点膨胀后白色区域变大能填补黑色的小洞。更常用的组合操作是开运算和闭运算。先腐蚀再膨胀叫开运算用来消除前景外的小噪点先膨胀再腐蚀叫闭运算用来填充前景内的小孔洞。OpenCV 提供了单行实现opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)morphologyEx的第二个参数是操作类型MORPH_OPEN和MORPH_CLOSE是最常用的两个。用MORPH_OPEN替换“先 erode 再 dilate”的两行代码代码语义更清晰性能也更好。我做轮廓提取前的 mask 处理时标准流程就是 Otsu 二值化 → 开运算去噪 → 闭运算补洞三步走完再找轮廓。4.3 寻找轮廓与绘制拿到你真正想要的区域分割完以后下一步通常是把连通区域找出来用外接矩形去框选目标。查找轮廓的代码充满陷阱我来写一份能直接跑通并看懂结果的版本import cv2 binary cv2.imread(mask.png, cv2.IMREAD_GRAYSCALE) contours, hierarchy cv2.findContours( binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) canvas cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR) cv2.drawContours(canvas, contours, -1, (0, 255, 0), 2) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) cv2.rectangle(canvas, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imshow(contours, canvas)findContours的第一个参数必须是二值图不能是灰度图或彩色图。第二个参数RETR_EXTERNAL表示只提取最外层轮廓如果改成RETR_LIST则提取所有轮廓并放在同级列表里。第三个参数CHAIN_APPROX_SIMPLE会压缩轮廓点数量只保留端点大幅减少内存占用。drawContours的第三个参数-1表示绘制所有轮廓第四个参数(0, 255, 0)是 BGR 颜色第五个参数 2 是线条粗细。cv2.boundingRect返回轮廓的最小外接矩形的 x、y 坐标和宽高它是轴对齐的不是最小面积外接矩形。想获得带旋转角度的最小外接矩形要用cv2.minAreaRect这个在需要框住倾斜目标时才会用到。代码里我把drawContours画在了彩色画布上这样绿色轮廓和红色矩形框同时显示能直观验证找到的轮廓是不是自己要的目标。如果轮廓数量多到爆炸优先排查输入图是不是经过了THRESH_OTSU和开闭运算这两个步骤对轮廓质量的贡献比findContours本身参数更大。5. OpenCV 基础代码的常见坑路径、通道、坐标与显示的排查记录5.1 中文路径读不出图imread 返回 None 的通用解法现象cv2.imread(图片/示例.jpg)不报错打印img显示None后续代码在访问img.shape时抛异常。原因OpenCV 的imread底层依赖 C 标准库的文件读取接口在 Windows 上对中文路径支持不完整而 Linux 或 macOS 上多数情况正常。这个现象我在某公司做图像处理 Demo 时遇到过多次和 OpenCV 版本关系不大。解决先用cv2.imdecode配合 numpy 读入字节流绕开路径编码问题。import cv2 import numpy as np def imread_unicode(filepath): stream open(filepath, rb) data np.frombuffer(stream.read(), dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) stream.close() return img img imread_unicode(图片/示例.jpg)np.frombuffer把文件读到的字节序列转成 numpy 数组cv2.imdecode对这个数组解码成图像不再依赖文件路径字符串。封装成函数后所有涉及中文路径的读取都走这个入口写图用cv2.imencode加tofile也能解决同样的问题。5.2 显示出的图像颜色不对BGR 与 RGB 的混用现象用matplotlib显示cv2.imread读进来的图画面整体偏蓝红色变蓝色蓝色变红色。原因OpenCV 读图后通道顺序是 BGR而matplotlib的imshow默认按 RGB 解释。两者没有谁对谁错只是约定不同。解决显示前主动转换通道顺序。import cv2 import matplotlib.pyplot as plt img cv2.imread(demo.jpg, cv2.IMREAD_COLOR) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(rgb) plt.axis(off) plt.show()cv2.COLOR_BGR2RGB做的事情只是把通道维度重新排列没有做颜色插值或变换。实际项目中如果要把处理结果保存成适合网页展示的图片也是在imwrite之前确认好通道顺序而不是保存后再补救。5.3 坐标写反x 是列y 是行现象框选目标时矩形位置总是偏离或者画出来的线在图像外。原因OpenCV 的图像坐标以左上角为原点x 轴向右y 轴向下。shape返回的是(height, width)而访问像素、画矩形时用的坐标是(x, y)即(width, height)方向。忘记这一点就会把行列和宽高搞混。解决固定一套写法。取图像尺寸用h, w img.shape[:2]之后所有画图操作都写(x, y)其中x对应列方向y对应行方向。如果写img.shape[0]当作 x 使用画出来的位置必然垂直方向偏转。5.4 waitKey 卡死或无响应窗口不刷新的两种典型情况现象cv2.imshow之后窗口一片灰色或者按什么键都没反应。原因第一种是忘记调用cv2.waitKey。imshow只是把图像数据交给窗口管理器真正的事件循环由waitKey启动。第二种是waitKey(0)之后没有处理返回键值就退出在某些 Linux 桌面环境里窗口事件没有正常分发。第三种是连续调用多个imshow每个窗口出现后没有及时更新系统把窗口标记为未响应。解决把显示逻辑收敛到一个函数里每次只调用一次waitKey。def show_images(title, images): for i, img in enumerate(images): cv2.imshow(f{title}_{i}, img) cv2.waitKey(0) cv2.destroyAllWindows()f{title}_{i}给每个窗口生成唯一名字避免同名窗口互相覆盖。waitKey(0)之后统一销毁所有窗口。如果还需要程序继续运行把waitKey(0)改成waitKey(1)并配合循环检查key ord(q)退出。5.5 二值化后目标变成黑白颠倒没理解 THRESH_BINARY_INV现象目标区域反而成了黑色背景成了白色后续找轮廓时发现轮廓全都框在背景上。原因THRESH_BINARY把大于阈值的置为 255如果目标比背景暗目标就会被置为 0黑色。这时需要的是反向二值化。解决先用直方图确认目标和背景谁更亮。import cv2 gray cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE) blurred cv2.GaussianBlur(gray, (5, 5), 0) ret, binary cv2.threshold( blurred, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU )cv2.THRESH_BINARY_INV和cv2.THRESH_OTSU用加号连接两个标志位同时生效。这个写法的好处是阈值仍然自动计算只是把结果黑白对调。如果加了INV后发现轮廓反而更乱说明目标本来就是亮的去掉INV即可。我通常会把两种模式都跑一遍肉眼确认后定稿不为省一次运行时间而盲目猜。6. 一个验证好习惯用像素差异对比处理前后的效果基础代码全部跑通之后需要建立“怎么评价处理效果”的意识。肉眼观察很重要但不可量化。我习惯用像素差异来验证处理流程是否真的在按预期工作这套方法在入门阶段就能用。import cv2 import numpy as np img cv2.imread(demo.jpg, cv2.IMREAD_GRAYSCALE) blurred cv2.GaussianBlur(img, (5, 5), 0) diff cv2.absdiff(img, blurred) print(平均像素差:, diff.mean())cv2.absdiff计算两张图逐像素的绝对差diff.mean()得到平均差异值。灰度图做高斯模糊后平均像素差通常在 5 到 20 之间如果超过 40说明核太大或图像里高频纹理特别多。这个数字可以当作处理强度的量化参考调整核大小时看这个值的走势比反复切窗口更可靠。进阶一点还可以用峰值信噪比来评估处理后的图像质量OpenCV 没有内置函数但公式很简单import cv2 import numpy as np def psnr(img1, img2): mse np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) if mse 0: return float(inf) return 20 * np.log10(255.0 / np.sqrt(mse)) psnr_value psnr(img, blurred) print(PSNR:, psnr_value)astype(np.float64)是为了防止无符号整数减法溢出这是新手最容易踩的数值陷阱。mse是均方误差PSNR 通常大于 30dB 时认为两张图差异很小。这个函数我每次处理完图像都会跑一遍记录处理前后的 PSNR方便对比不同参数的效果。我在给 A 同学讲完这套基础后让他拿着同一张图分别做模糊、Canny、二值化、形态学四组处理每组记录平均像素差和 PSNR做完再回到原图看视觉对比。这个习惯我一直保留因为处理代码能不能跑通是一回事处理效果有没有达到预期是另一回事量化指标能让后者不再靠猜。字符串拼装的路径、忘掉的waitKey、颠倒的通道顺序这些坑踩过一次就长记性了但量化验证的思维需要刻意培养。希望这篇笔记能帮你把基础代码练扎实少走我当年绕过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?