ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCV与PIL图像基础:从读写到PyTorch边缘检测的完整实战

OpenCV与PIL图像基础:从读写到PyTorch边缘检测的完整实战 上次我们把 PyTorch 的环境跑通了这次继续往前走把图像处理的两大基础库 OpenCV 和 PIL 装好再拿一个真正能跑的小实战练手。很多零基础的同学在这里会卡住不是 PyTorch 不会用而是读进来的图片到底是个 numpy 数组还是 PIL Image颜色通道到底是 BGR 还是 RGB转成 tensor 之后形状为什么是[3, 224, 224]而不是[224, 224, 3]。这篇文章就是把这些绕不过去的坑一次性讲清楚然后带着你从生成测试图开始用 PyTorch 做一次卷积边缘检测用 OpenCV 检测直线再顺手做个骨架提取全套流程你照着敲就能跑通。看完之后你不仅能理解 OpenCV 和 PIL 在日常图像处理里各自扮演什么角色还能搞明白它们和 PyTorch 之间是怎么衔接的。适合刚接触 PyTorch、准备做计算机视觉项目或者只会调用cv2.imread但不懂背后原理的人。内容不跳步每个关键点都给了原因和验证方法。1. 先说清楚OpenCV 和 PIL 都是干什么的为什么两个都要装1.1 两个库的定位差异一个是工具箱一个是轻量读写器PIL现在的维护版本叫 Pillow在 PyTorch 生态里的角色更像是一张“照片入口”。它负责读图片、写图片、转格式、缩缩放放、简单画线画框API 设计非常友好。你打开一个图片就是Image.open(x.jpg)保存就是img.save(y.png)。PyTorch 的torchvision.datasets很多内置数据集返回的就是 PIL Imagetransforms.ToTensor()也默认处理 PIL Image。OpenCV 则是计算机视觉的“瑞士军刀”。它有上千个内置算法包括边缘检测、直线检测、轮廓查找、形态学操作、视频读取、摄像头采集、图片颜色空间转换、滤波去噪等等。你跑深度学习模型之前要做预处理、要可视化检测结果、要处理视频流OpenCV 都是绕不开的。说得再直白一点PIL 是“能把图片打开和保存的轻量工具”OpenCV 是“能对图片做各种加工的重型工作台”。两者不是替代关系而是配合关系。我在实际项目里最常见的一段开场就是from PIL import Image import numpy as np img_pil Image.open(demo.png) # 用 PIL 读取 rgb np.array(img_pil.convert(RGB)) # 转成 numpy后面交给 opencv 处理后面接 OpenCV 滤波、PyTorch 张量化、进入神经网络整条流水线分工很明确。如果你只装了其中一个遇到torchvision.transforms要 PIL Image、OpenCV 读出来却是 numpy 数组的时候就会到处找转换代码反而把自己绕晕。所以零基础入门两个一起装少很多折腾。1.2 为什么选择 OpenCV PIL 组合而不是二选一有人可能会问OpenCV 也能读图片、也能保存为什么还要加 PIL因为我们确实会遇到只有 PIL 更方便的场景。第一个场景是数据集加载。torchvision.datasets.ImageFolder默认就是用 PIL 读图你如果只用 OpenCV还得额外包一个 Dataset 把 numpy 转成 PIL 或者直接转 tensor多写一层代码。第二个场景是画图。PIL 从零生成一张测试图特别省事几行代码就能画出直线、矩形、圆适合做单元测试和算法验证。OpenCV 也能画但写起来总有一种写 C 的感觉。第三个场景是颜色通道。OpenCV 的历史原因导致它默认用 BGR 通道顺序而 PIL 用 RGB。新手第一次用 OpenCV 读图再保存会发现颜色偏蓝偏红其实就是通道顺序搞反了。PIL 没有这个问题它更贴合人类对颜色的直觉。我自己的习惯是凡是“要把图片喂给 PyTorch”的流程优先用 PIL 读凡是“要对图片做检测、找轮廓、算边缘”的流程优先用 OpenCV 处理最后保存结果时再回到 PIL 输出因为Image.fromarray()在 RGB 模式下颜色一定是对的。这样分工能避开一大半颜色通道的坑。为了让你更直观地理解两个库的侧重点我整理了一张对比表对比项Pillow (PIL)OpenCV定位轻量图像读写与基础处理计算机视觉算法库读图返回类型PIL Image 对象numpy.ndarray颜色通道RGBBGR视频/摄像头支持不支持支持常见算法少边缘检测、直线检测、轮廓、特征点等和 torchvision 结合原生支持需要转换适合场景读图、保存、简单绘制、数据增强图像预处理、检测、视频处理2. 环境准备零基础也能一次装对的安装思路2.1 Anaconda 下创建独立环境别把包装乱我用 Anaconda 做演示因为它自带 conda、pip、jupyter而且能创建多个互相隔离的 Python 环境。你上一篇文章如果已经装过一个 PyTorch 环境这次直接在同一个环境里补装 OpenCV 和 Pillow 就行不一定要新建。但如果你之前乱装过很多包我建议还是新建一个干净环境避免版本冲突。打开命令行Windows 用 Anaconda PromptmacOS/Linux 用终端执行conda create -n cv python3.9 -y conda activate cv这里我选 Python 3.9主要原因是兼容性最稳。PyTorch 从 1.13 到 2.xPython 3.8 到 3.11 基本都能跑但最新的 Python 3.12 有些老版本 wheel 可能不全新手没必要追求最新。Python 只是工具跑得稳比跑得新重要。激活环境之后先用python --version确认当前 Python 是 3.9再用which pythonWindows 是where python确认你用的解释器在 Anaconda 的环境目录里。这一步能避免后面“明明装了这个包却 import 不到”的尴尬。2.2 PyTorch 与 Python 版本的对应关系PyTorch 官方对不同 Python 版本有对应的安装包。你只需要记住一个原则不要用 pip 乱装最新版先打开 PyTorch 官网的安装页选择自己的操作系统、包管理器和 CUDA 版本复制它生成的命令就可以。如果你的电脑有 NVIDIA 显卡想用 GPU先运行nvidia-smi看右上角的 CUDA Version。这只是驱动支持的 CUDA 版本不代表你必须装那么高的 PyTorch CUDA 版本。一般装了 CUDA 11.8 对应的 PyTorch 就能覆盖绝大多数 30 系、40 系显卡。常见做法是pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果你的电脑没有 NVIDIA 显卡或者不打算跑大模型直接用 CPU 版pip install torch torchvision torchaudio千万不要有“安装 PyTorch 是不是必须要有 GPU”的焦虑。CPU 版完全可以做图像分类、边缘检测、小规模实战。GPU 只是把训练和卷积加速不代表你学不会原理。把 CPU 版跑通之后再用 GPU 也只是换一条安装命令的事。2.3 opencv-python 和 opencv-contrib-python 怎么选图像处理库的安装有一个特别常见的坑OpenCV 有两个 pip 包一个叫opencv-python一个叫opencv-contrib-python。前者是最核心的模块后者在核心模块基础上额外包含了ximgproc骨架提取、SLIC 超像素等、xfeatures2dSIFT、SURF等扩展算法。如果只做入门opencv-python就够了。但这次小实战里我要用到cv2.ximgproc.thinning这个函数不在核心包里所以直接装opencv-contrib-pythonpip install opencv-contrib-python同时把 Pillow 也装上pip install pillow这里有一个重要提醒opencv-python和opencv-contrib-python不要同时装这两个包会互相覆盖同名文件最后可能导致import cv2直接报错或部分函数失灵。你在安装前可以先看下当前环境有没有装过pip list | grep opencv如果有先卸载再装你要的那一个。至于opencv-python-headless那是给服务器和无显示器环境用的没有 GUI 显示能力本地学习装普通版就行。3. 图像处理基石三行代码搞懂读写、颜色通道和数组转换3.1 cv2.imread 和 PIL.Image.open 的区别很多教程写cv2.imread读图但我们要先搞清楚它和 PIL 读图到底有什么不同。import cv2 from PIL import Image # OpenCV 读出来的是 numpy 数组 img_cv cv2.imread(demo.png) print(type(img_cv)) # class numpy.ndarray print(img_cv.shape) # (高, 宽, 通道数) # PIL 读出来的是 PIL Image 对象 img_pil Image.open(demo.png) print(type(img_pil)) # class PIL.PngImagePlugin.PngImageFile print(img_pil.size) # (宽, 高)注意cv2.imread如果文件路径不对不会抛异常而是返回None。你如果不对None做判断后面img.shape会报AttributeError: NoneType object has no attribute shape。而Image.open路径不对会直接抛FileNotFoundError。两者的错误风格完全不同这也影响了后续排查方式。另一个区别是 shape 的维度顺序。OpenCV 返回的是(H, W, C)也就是高度在前、宽度在后。PIL 的size却是(W, H)。新手经常在这里蒙圈img.shape的宽和高正好和img.size反着。你只要记住一个规则numpy 统一按“先行后列”也就是先高后宽PIL 按“先宽后高”。后面转成 PyTorch tensor 时还会遇到通道在前的问题所以每一步都要用print验证形状。3.2 BGR / RGB / 灰度颜色通道顺序是第一个坑OpenCV 的imread默认把图片解码成 BGR 颜色空间而不是常见的 RGB。这就是为什么你用 OpenCV 读图后用 matplotlib 显示图像颜色会发蓝因为 matplotlib 期望 RGB你给的却是 BGR。转换方法有两种。第一种是严格型img_rgb cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)第二种是取巧型直接在 numpy 上反转通道img_rgb img_cv[:, :, ::-1][:, :, ::-1]就是把第三个维度也就是通道维度倒过来。BGR 反序自然变成 RGB。这个方法快但读起来没有cvtColor那么明确我建议你在正式代码里用cvtColor语义更清晰。如果要做灰度图OpenCV 用gray cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)PIL 用gray_pil img_pil.convert(L)灰度图只有一个通道形状会从(H, W, 3)变成(H, W)这也是后面 PyTorch 卷积操作的常见输入格式。3.3 图像转 PyTorch TensorHWC 到 CHW 的完整流程PyTorch 的卷积神经网络默认输入形状是(N, C, H, W)N 是 batch sizeC 是通道数H 是高度W 是宽度。而 OpenCV 和 PIL 读进来的 numpy 数组是(H, W, C)。所以必须做一次维度重排。使用 PIL 和 torchvision 是最顺滑的from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) tensor transform(img_pil) print(tensor.shape) # torch.Size([3, 224, 224])ToTensor()做了两件重要的事第一把 PIL Image 或 numpy 数组从HWC变成CHW第二把像素值从 0-255 归一化到 0-1。这两个动作很容易被忽略但对模型训练至关重要。如果你手里是 OpenCV 读出来的 BGR numpy 数组千万不要直接丢给ToTensor()否则你的通道顺序就错了。正确做法是先用cvtColor转成 RGB再torch.from_numpy之后手动permuteimg_rgb cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(img_rgb).permute(2, 0, 1).float() / 255.0 print(tensor.shape) # torch.Size([3, H, W])这里的permute(2, 0, 1)是把原来索引为 2 的通道维度挪到最前面。这一步操作是整个视觉项目中出现频率最高的“魔法代码”建议你亲手拆一遍先创建一个(H, W, 3)的小数组打印各维度索引再做 permute观察形状变化记忆会深刻得多。4. 小实战用 PIL 生成图PyTorch 做边缘检测OpenCV 找直线和骨架4.1 准备一张测试图用 PIL 直接画出十字线实战不需要外面找数据集直接用 PIL 生成一张白底黑色十字线这样所有参数都可控便于你调试。from PIL import Image, ImageDraw img Image.new(RGB, (600, 400), white) draw ImageDraw.Draw(img) draw.line((100, 200, 500, 200), fillblack, width8) draw.line((300, 100, 300, 350), fillblack, width8) img.save(demo.png)这张图里有两条直线一条水平、一条竖直。后面无论是 Sobel 边缘检测还是 HoughLinesP 直线检测都能得到非常明确的结果。建议你运行完这段代码后把图片打开看一眼记住它的样子。4.2 用 PyTorch 的 F.conv2d 实现 Sobel 边缘检测很多人只知道 PyTorch 能训练模型不知道它也能直接做底层卷积运算。这里我们就用torch.nn.functional.conv2d手写一个 Sobel 算子验证一下“卷积”到底是怎么工作的。Sobel 算子分 x 方向和 y 方向x 方向的卷积核是-1 0 1 -2 0 2 -1 0 1y 方向是-1 -2 -1 0 0 0 1 2 1在 PyTorch 里图片要先从(H, W)变成(1, 1, H, W)因为conv2d要求输入是四维的(batch, channel, height, width)。import cv2 import numpy as np import torch import torch.nn.functional as F img_pil Image.open(demo.png) rgb np.array(img_pil.convert(RGB)) gray cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY) blur cv2.GaussianBlur(gray, (3, 3), 0) tensor torch.from_numpy(blur).float().unsqueeze(0).unsqueeze(0) # (1,1,H,W) print(tensor.shape) # torch.Size([1, 1, 400, 600]) sobel_x torch.tensor([[[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]]], dtypetorch.float32) sobel_y torch.tensor([[[[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]]]], dtypetorch.float32) edge_x F.conv2d(tensor, sobel_x, padding1) edge_y F.conv2d(tensor, sobel_y, padding1) edge torch.sqrt(edge_x ** 2 edge_y ** 2) edge edge.squeeze().numpy() edge (edge / edge.max() * 255).astype(np.uint8)padding1是为了让卷积前后尺寸不变。如果不加 padding输入 400x600、卷积核 3x3输出会变成 398x598很多新手调试时发现图片尺寸对不上就是这里出了问题。卷积完用平方和开方把 xy 两个方向的边缘强度合成一个梯度幅值这就是 Sobel 边缘检测的经典做法。4.3 接上 OpenCV 的 HoughLinesP 检测直线拿到边缘图之后先用阈值二值化再用 OpenCV 的HoughLinesP把直线找出来。_, binary cv2.threshold(edge, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) lines cv2.HoughLinesP(binary, 1, np.pi / 180, threshold50, minLineLength30, maxLineGap5) result rgb.copy() if lines is not None: for line in lines: x1, y1, x2, y2 line[0] cv2.line(result, (x1, y1), (x2, y2), (255, 0, 0), 3)HoughLinesP的几个参数值得解释一下。第一参数是像素单位的分辨率1 就表示步长是一个像素。第二参数是角度分辨率np.pi/180表示 1 度。第三个threshold是投票阈值只有投票数超过 50 的直线才会被保留值越小检测出的直线越多但也更容易把噪声当直线。minLineLength是最小直线长度太短的不算直线maxLineGap是同一线段上允许的最大间隔它能把断断续续的边缘连成一条线。针对我们的十字线图threshold50、minLineLength30已经足够。这里用result是 RGB 数组但cv2.line并不关心颜色空间给它(255, 0, 0)就是红色因为 RGB 下红色就是(255, 0, 0)。如果你用 OpenCV 的imread读图(255, 0, 0)在 BGR 下是蓝色这点又回到了颜色通道的问题。4.4 用 ximgproc.thinning 再做骨架提取骨架提取也叫细化是把一个二值形状压缩成单像素宽的骨架常用于 OCR 字符识别、笔画提取、路径规划。OpenCV 的ximgproc扩展模块提供了现成的thinning函数。import cv2.ximgproc skeleton cv2.ximgproc.thinning(binary, thinningTypecv2.ximgproc.THINNING_GUOHALL)thinningType可以选THINNING_ZHANGSUEN或THINNING_GUOHALL两种都是经典的细化算法。Zhang-Suen 是每次迭代剥掉边界像素但保持连通性Guo-Hall 对直线交叉点的处理略有不同。你不需要记算法细节只需要知道它们最后都会输出单像素宽的结果。注意这段代码在裸opencv-python里是不存在的必须安装opencv-contrib-python这也是我前面让你装它的原因。如果确实不想装 contrib也有替代方案循环执行cv2.erode配合连通性判断但代码会复杂得多学习阶段没有必要重复造轮子。最后把结果拼在一起保存result_pil Image.fromarray(np.hstack([result, cv2.cvtColor(skeleton, cv2.COLOR_GRAY2RGB)])) result_pil.save(result.png)这里用np.hstack把边缘检测结果和骨架图横向拼成一张对比图再用 PIL 保存。使用 PIL 而不是cv2.imwrite最大的好处是颜色通道不会出错RGB 数组保存成 PNG 后颜色和预期完全一致。5. 实战中高频踩坑与排查手册5.1 ModuleNotFoundError: No module named cv2这是出现频率最高的问题。你在终端里明明装了opencv-python但运行脚本时 Python 说找不到cv2。最常见原因是当前激活的环境和你安装包的环境不是同一个。排查顺序两步走。第一步在脚本开头的终端里运行python -m pip show opencv-contrib-python如果显示版本和路径说明包确实在当前 Python 环境里。第二步再用:where python看当前 Python 解释器路径。如果你看到多个 Python 路径说明系统里有好几个 Pythonpip 装到了 A 环境而你的脚本用 B 环境的解释器在跑。解决办法很简单只用python -m pip install不要直接写pip install前者一定会把包装到当前python对应的环境里。5.2 装了 OpenCV 却还是 ImportError这种问题比“没装”更让人头疼。常见情况有三种。第一种是当前环境里同时存在opencv-python和opencv-contrib-python两个包互相覆盖导致某些模块缺失或import cv2直接报错。解决办法是卸载两个只装一个pip uninstall opencv-python opencv-contrib-python pip install opencv-contrib-python第二种是装成了opencv-python-headless。这个包没有 GUI 相关功能某些版本里cv2.imshow会报错尽管import cv2不报错。如果你是服务器环境headless 可以用本地调试最好换成普通版。第三种是编辑器比如 PyCharm使用的解释器不是当前 conda 环境。打开 PyCharm 的 Settings - Project - Python Interpreter确认选中的解释器就是你执行conda activate cv之后which python看到的路径。这个坑和上一类问题本质上是一回事。5.3 cv2 里的函数名大小写contourArea 的教训热词里有一个很典型的问题contourarea ()未定义标识符。这其实是 OpenCV API 的函数名大小写问题。OpenCV 的 Python 接口是从 C 绑定过来的函数名严格区分大小写。找轮廓的面积函数叫cv2.contourArea不是cv2.contourarea也不是cv2.ContourArea。如果你在 PyCharm 里看到“未定义标识符”先检查函数名的大小写。OpenCV 里很多函数都是驼峰命名比如cv2.cvtColor、cv2.GaussianBlur、cv2.HoughLinesP中间字母的大小写不能错。类似的还有cv2.findContours不要写成cv2.FindContours。如果你是自己手敲代码建议尽量用 IDE 的自动补全能少打错一半字母。5.4 PyTorch 安装相关GPU、WSL、版本对应“安装 PyTorch 是不是必须要 GPU”是我见过最多的疑虑。完全不是。CPU 版能跑绝大多数入门实战包括本文的卷积边缘检测。GPU 只是把矩阵运算加速不是必需项。你现在要学的是原理和代码CPU 足够。WSL 用户要多留个心眼。在 Windows 的 WSL 里PyTorch 的 GPU 支持受限于 WSL 版本、显卡驱动、CUDA 版本折腾成本很高。建议第一次跑项目直接用 CPU 版先确认代码正确再考虑 GPU。如果你用的是 AMD 显卡Windows 下 PyTorch 官方目前没有原生 ROCm 支持想用 GPU 往往要研究很长的环境配置属于进阶玩法零基础阶段可以直接跳过。版本对应问题同样用一句话概括PyTorch 版本、Python 版本、CUDA 版本三者互相限制不要自己乱拼。以官网安装页生成的命令为准比任何博客教程都可靠。最后分享几个我自己的操作习惯这套流程我重复过很多次踩过的坑比上面写的还要多。现在固定下来的习惯是第一步永远print一下当前变量类型和shape不要凭感觉往下写第二步优先用Image.open读原始图遇到需要颜色转换时再交给 OpenCV第三步所有结果图都用 PIL 的Image.fromarray保存彻底绕开 BGR/RGB 的烦恼。还有一个小技巧是先把测试图缩到 200 像素宽跑通流程再换原图调试速度会快很多。希望这篇内容能帮你把 OpenCV、PIL、PyTorch 三个工具真正串起来之后做数据增强、模型训练预处理时你会发现自己已经站在一个很稳的地基上了。
返回列表