简介基于Python的数字图像处理课程设计资料包面向学习OpenCV与Numpy的计算机视觉初学者也适合需要课程设计、实验参考或期末项目的在校生。内容覆盖彩色图像灰度化、卷积与相关操作、高斯核滤波、二维傅里叶变换及逆变换、频谱中心化与整数次幂填充等关键知识点有助于从空间域到频域完整理解图像处理原理并快速迁移到实际代码中。压缩包共17个文件约3.57MB主体为Python脚本与Jupyter Notebook源码另配9幅TIF/TIFF测试图像及PNG样例、README、License说明等目录简洁便于直接运行和对比结果。资源已有825人学习源码与真实图像数据均可直接复现灰度化、滤波、频域分析等效果可作为课程报告、实验演示或后续研究的基础工具。1. 基于Python数字图像处理灰度化、卷积与傅里叶变换一整套可复现的课程设计这套课程设计资源拿到手是一份能直接跑起来的完整工程一个 ImageProcess.py 主脚本、两个拆分好的 notebookwork_1.ipynb 和 work_2.ipynb外加十一张标准测试图像从 cameraman、lena 到月面图像一应俱全。它把数字图像处理课程里最常考、也是面试笔试最爱问的几个模块——彩色转灰度、卷积与相关、高斯核平滑、二维傅里叶变换、频域中心化、频谱可视化、2 的幂次填充——全部用 OpenCV 加 Numpy 从零实现了一遍。适合正在赶课程设计的学生也适合想快速把冈萨雷斯《数字图像处理》里的原理落成代码的从业者。真正卡人的往往不是算法本身而是 BGR 通道顺序、filter2D 边界模式、FFT 低频分量这类细节后面逐一拆开讲。2. 环境与测试图集先解决 BGR 通道顺序再谈灰度化2.1 用 conda 搭出可复现的 Python 图像处理环境网上 python 安装教程很多但图像处理实验我一般不会往系统 Python 里直接塞包而是用 conda 单独建一个环境避免 numpy 和 OpenCV 的版本互相打架。推荐 Python 3.10 起步这个版本对 opencv-python 和 jupyter 的兼容性都稳定。conda create -n dip python3.10 -y conda activate dip pip install opencv-python numpy jupyter jupyter notebook逻辑说明先创建名为 dip 的独立环境再安装四个核心依赖。opencv-python 提供 cv2 模块numpy 负责矩阵运算与 FFTjupyter 用来打开资源包里的 work_1.ipynb 和 work_2.ipynb。这里刻意只装 opencv-python不装 opencv-contrib-python因为 contrib 里包含的 xphoto、optflow 等扩展模块在课程设计里基本用不到装了反而可能和其他包产生命名冲突。环境配好后先把 ImageProcess.py 和两个 notebook 放在同一个目录下再放测试图像。如果你用的是 VS Code记得在 .vscode/settings.json 里指定 python.defaultInterpreterPath 指向 conda 环境的 python.exe否则终端能 import cv2、notebook 里却报 ModuleNotFoundError这种翻车我在不同机器上见过太多次。验证安装是否成功在终端跑一句python -c import cv2, numpy; print(cv2.__version__, numpy.__version__)能同时打出两个版本号就说明环境没问题。2.2 十一张测试图像每张图分别验证什么资源包里附带的图像选得很有针对性覆盖了灰度图、彩色图、纹理图、医学图像和月面遥感图几大类。我建议按下面的映射关系去用而不是每张图都从头到尾跑一遍图像文件类型适合验证的内容cameraman.tif灰度图边缘检测、频域高通滤波的经典素材lena_gray_512.tif灰度图灰度化对照、高斯平滑、FFT 频谱分析lena512color.tiff彩色图BGR 与 RGB 通道顺序验证mandril_color.tif彩色纹理图彩色转灰度、高频细节保留效果rose512.tif彩色图色彩空间转换、通道分离house.tif / house02.tif灰度图卷积核边界效应、图像锐化lunar_surface.tif遥感灰度图16 位深度读取、频谱能量分布Characters_test_pattern.tif测试图卡2 的幂次填充、频域滤波对比einstein.tif灰度图混合滤波、去噪前后对照1.pngPNG 格式读图路径与格式兼容性测试这张表是我自己整理的习惯用法不是资源包里的固定要求。核心思路是跑通用流程用 cameraman 和 lena测色彩相关操作用 mandril_color 和 rose512测深度和格式兼容性用 lunar_surface 和 Characters_test_pattern。每完成一个小节固定用对应图像验证排查问题时更容易定位到是代码问题还是输入图问题。2.3 彩色转灰度cvtColor 与 imread 灰度模式的差异彩色转灰度在数学上是对 R、G、B 三个通道做加权平均OpenCV 使用的权重公式是 Y 0.299R 0.587G 0.114B。但这里有个新手必踩的坑OpenCV 读进来的默认通道顺序是 BGR不是 RGB。也就是说 cvtColor 内部其实是按 B 通道乘 0.114、G 通道乘 0.587、R 通道乘 0.299 来算的顺序反了结果完全不对。import cv2 import numpy as np img_bgr cv2.imread(mandril_color.tif, cv2.IMREAD_COLOR) gray_cvt cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) gray_imread cv2.imread(mandril_color.tif, cv2.IMREAD_GRAYSCALE) print(cvtColor 结果 shape:, gray_cvt.shape) print(imread 灰度模式 shape:, gray_imread.shape) print(两种方式结果是否完全一致:, (gray_cvt gray_imread).all())逻辑说明第一行用 IMREAD_COLOR 把彩色图读成三通道 BGR第二行用 cvtColor 做加权灰度转换第三行用 IMREAD_GRAYSCALE 直接读灰度图。最后一行对比两个结果是否逐像素一致。参数说明IMREAD_COLOR 固定读成三通道忽略 alpha 通道IMREAD_GRAYSCALE 在读入阶段就完成灰度化IMREAD_UNCHANGED 保留原始所有通道与位深读 16 位 tif 时要用这个标志。实际运行后你会发现两个结果完全一致因为 imread 的灰度模式底层调用的就是同一套加权转换。区别在于如果你后续还要用彩色信息做别的处理就要先读彩色图再手动 cvtColor如果只需要灰度图直接 IMREAD_GRAYSCALE 更省内存。血泪经验用 IMREAD_UNCHANGED 读 lunar_surface.tif 这类 16 位图拿到的是 uint16 数据后续做卷积和 FFT 时数值范围、显示效果和 8 位图完全不同容易踩坑。3. 卷积与相关空间域处理里最容易混淆的两个操作3.1 卷积核为什么要翻转以及 filter2D 实际做的是什么卷积和相关在数学上只差一个操作卷积要把核旋转 180 度后再做滑动窗口乘法求和相关则不做翻转。对高斯核这种中心对称的核两者结果一样对非对称核就不同了。数字图像处理课程里经常出现一个让人懵的点cv2.filter2D 名义上叫 filter2D实际执行的是相关而不是严格数学意义上的卷积。计算相关时输出图像每个像素等于核窗口内像素与核系数的加权和核不翻转严格卷积需要先翻转核。OpenCV 选择用相关实现是因为相关运算在数学上更直观而且对对称核没有区别。import cv2 import numpy as np img cv2.imread(lena_gray_512.tif, cv2.IMREAD_GRAYSCALE) sobel_x np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtypenp.float32) corr_result cv2.filter2D(img, -1, sobel_x) conv_result cv2.filter2D(img, -1, cv2.flip(sobel_x, -1)) diff np.abs(corr_result.astype(np.int16) - conv_result.astype(np.int16)) print(相关与卷积结果差异像素数:, (diff 0).sum())逻辑说明sobel_x 是经典的横向边缘检测核中心对称性不成立所以相关和卷积结果有差异。先用 filter2D 直接做相关再用 flip 把核在水平和垂直方向都翻转模拟严格卷积然后统计两幅结果图像的差异像素数。参数说明filter2D 的第二个参数 ddepth 设成 -1 表示输出图像深度和输入一致。这里把结果转成 int16 再做差是为了防止 uint8 相减出现负数回绕——如果直接用 uint8 相减负数会变成 255 附近的数值统计就全乱了。实际工程里边缘检测用相关还是卷积差别不大因为最后往往要取绝对值来获得边缘强度。但如果你做的是模板匹配或方向敏感的特征提取就必须搞清楚核的朝向。3.2 高斯核卷积从数学公式到 filter2D 的参数设置高斯平滑是去噪最常用的手段核心思想是用加权平均替代简单平均权重服从二维高斯分布离中心越近权重越大这样在去噪的同时能尽量保留边缘。OpenCV 给高斯平滑开了两个入口cv2.GaussianBlur 封装好的高层接口以及 cv2.getGaussianKernel 加 cv2.filter2D 的手工组合。import cv2 import numpy as np img cv2.imread(cameraman.tif, cv2.IMREAD_GRAYSCALE) ksize 5 sigma 1.2 # 方案A直接调用封装好的高斯滤波 blur_a cv2.GaussianBlur(img, (ksize, ksize), sigma) # 方案B手工生成高斯核再用 filter2D 做卷积 kernel_x cv2.getGaussianKernel(ksize, sigma) kernel_2d kernel_x kernel_x.T blur_b cv2.filter2D(img, -1, kernel_2d, borderTypecv2.BORDER_REFLECT) print(二维高斯核:) print(kernel_2d) print(两种方案结果是否一致:, (blur_a blur_b).all())逻辑说明getGaussianKernel 生成一个一维高斯核形状是 (ksize, 1)通过矩阵乘法 kernel_x kernel_x.T 和二维可分离高斯核等价。GaussianBlur 内部也走同样的计算路径只是它把核生成和卷积封装到了一起所以两种方案结果应该完全一致。参数说明ksize 必须为正奇数常见值是 3、5、7核越大平滑越强但细节损失越多。sigma 是高斯函数的标准差控制权重的衰减速度。sigma 设得越小权重越集中在中心点图像越接近原图设得越大平滑范围越广。一个玄学点sigma 和 ksize 不匹配时OpenCV 会自动用 sigma 去反推截断范围所以如果你指定 sigma1.2 但 ksize21实际核中间大部分区域权重几乎为零白白浪费计算量。3.3 边界填充零填充、镜像填充与反射填充对边缘像素的影响卷积操作的边界处理是最容易被忽略但最影响视觉效果的地方。图像边缘的像素没有完整的邻域filter2D 必须从外部拿值来补。OpenCV 提供了多种 borderType课程设计里足够用的是下面这三种import cv2 import numpy as np img cv2.imread(lena_gray_512.tif, cv2.IMREAD_GRAYSCALE) kernel cv2.getGaussianKernel(5, 1.2) kernel_2d kernel kernel.T edge_zero cv2.filter2D(img, -1, kernel_2d, borderTypecv2.BORDER_CONSTANT) edge_replicate cv2.filter2D(img, -1, kernel_2d, borderTypecv2.BORDER_REPLICATE) edge_reflect cv2.filter2D(img, -1, kernel_2d, borderTypecv2.BORDER_REFLECT) # 检查边缘区域差异 roi_zero edge_zero[:5, :5].astype(np.int16) roi_reflect edge_reflect[:5, :5].astype(np.int16) print(左上角 5x5 区域零填充与镜像填充差异:, (roi_zero - roi_reflect).sum())逻辑说明三行代码分别用零填充、复制填充和镜像填充做同一高斯卷积然后对比左上角边缘区域。BORDER_CONSTANT 用固定值默认黑色填充外部BORDER_REPLICATE 把边缘像素值向外复制BORDER_REFLECT 像照镜子一样反射边缘像素。参数说明filter2D 默认的 borderType 是 BORDER_REFLECT_101它和 BORDER_REFLECT 的差别在于反射时不重复边缘那一行像素。视觉上 BORDER_REFLECT_101 更自然能避免边缘出现明显的高光条。需要注意后两者在傅里叶变换里会引入额外的频率分量如果做完空间域滤波还要做 FFT 频谱分析最好把边界填充方式也写清楚否则对比谱图时会有莫名其妙的多余高频成分。4. 二维傅里叶变换从空间域到频域的可视化与逆变换4.1 为什么要做 fftshift 中心化二维傅里叶变换把图像从空间域变换到频率域输出是一个和原图同尺寸的复数矩阵。np.fft.fft2 计算完成后低频分量也就是 DC 直流分量分布在矩阵的四个角高频分量集中在中心附近。这种布局适合数学推导但不符合视觉习惯而且做频域滤波时通常希望低频在中心所以要用 fftshift 把低频移到中央。import cv2 import numpy as np img cv2.imread(cameraman.tif, cv2.IMREAD_GRAYSCALE).astype(np.float32) f np.fft.fft2(img) fshift np.fft.fftshift(f) print(原始 FFT 四个角的幅度:, np.abs(f[:5, :5]).mean()) print(中心化后中心区域幅度:, np.abs(fshift[128:133, 128:133]).mean())逻辑说明cameraman.tif 是 256x256 图像fft2 后低频分布在四角fftshift 后低频集中到中心位置中心区域幅度远高于四角。中心化的物理含义就是把零频移到图像中心。参数说明fftshift 默认对所有维度进行移位二维图像直接调用即可。逆变换时对应使用 ifftshift顺序不能反。这里先把图像转成 float32 再做 FFT是因为 FFT 对 uint8 输入会先把数据提升到 float64 运算但提前转换能避免某些 OpenCV 版本里输入类型判断异常的问题。4.2 频谱可视化为什么不能直接显示 FFT 的幅度FFT 结果是一个复数矩阵直接取绝对值后数值跨度极大——DC 分量可能达到几千万而高频细节只有几百。如果用线性映射直接显示整张图除了中心一个亮点外全部是黑色细节全部丢失。所以频谱可视化的标准做法是取对数压缩动态范围。import cv2 import numpy as np img cv2.imread(cameraman.tif, cv2.IMREAD_GRAYSCALE).astype(np.float32) fshift np.fft.fftshift(np.fft.fft2(img)) magnitude np.abs(fshift) magnitude_log np.log1p(magnitude) # 归一化到 0~255 显示 magnitude_norm cv2.normalize(magnitude_log, None, 0, 255, cv2.NORM_MINMAX) spectrum magnitude_norm.astype(np.uint8) cv2.imwrite(spectrum_cameraman.png, spectrum) print(对数变换前幅度最大值:, magnitude.max()) print(对数变换后幅度最大值:, magnitude_log.max())逻辑说明np.log1p 是 ln(1x)加 1 是为了避免 0 值取对数出现负无穷。magnitude_log 把原本几千万的 DC 分量压缩到可显示范围再用 normalize 做线性拉伸到 0-255。如果不做对数变换display 出来的图像几乎全黑。参数说明normalize 的 NORM_MINMAX 模式会把数组最小值映射到 0、最大值映射到 255。需要注意归一化之后的数组是 float64转到 uint8 必须先 astype否则 imwrite 写出的图片是 16 位深很多看图软件打不开或者显示异常。如果觉得 log1p 之后图像仍然偏暗可以再乘一个缩放系数或者用 np.sqrt 代替 log两种压缩强度不同。4.3 逆变换ifftshift 与 ifft2 的顺序以及重建误差的来源频域处理的最后一步通常是把处理后的频域分量逆变换回空间域。顺序是先 ifftshift 把低频分量从中心移回四角再 ifft2 做逆变换。这个顺序经常被写反一旦写反恢复出的图像会出现中心旋转 180 度的错位。import cv2 import numpy as np img cv2.imread(cameraman.tif, cv2.IMREAD_GRAYSCALE).astype(np.float32) fshift np.fft.fftshift(np.fft.fft2(img)) # 光学低通保留中心区域 rows, cols img.shape crow, ccol rows // 2, cols // 2 lowpass_mask np.zeros((rows, cols), dtypenp.float32) lowpass_mask[crow-30:crow30, ccol-30:ccol30] 1 filtered_shift fshift * lowpass_mask f_ishift np.fft.ifftshift(filtered_shift) img_recon np.fft.ifft2(f_ishift) img_real np.real(img_recon) img_real_norm cv2.normalize(img_real, None, 0, 255, cv2.NORM_MINMAX) out img_real_norm.astype(np.uint8) cv2.imwrite(lowpass_cameraman.png, out)逻辑说明这段代码演示了完整的频域滤波链路——计算 FFT、中心化、构造低通掩码、频域乘法、去中心化、逆变换。ifftshift 的位置在乘法之后、逆变换之前把处理完的中心化频谱还原成四角布局ifft2 才能正确回到空间域。参数说明lowpass_mask 是理想低通滤波的矩形掩码半径为 30 像素。注意理想矩形截止会产生振铃效应这是傅里叶变换的性质导致的不是代码 bug。实际项目中更推荐用高斯低通掩码替代后面章节展开讲。逆变换输出是复数直接取实部 np.real 会保留负数值必须归一化到 0-255 再转 uint8直接 astype 会导致负值变成 255 数值而产生大片白色噪点。5. 避坑数字图像处理课程设计里最常翻车的五个细节5.1 用 IMREAD_UNCHANGED 读 16 位 tif后续处理结果全是黑的现象读入 lunar_surface.tif 后imshow 显示正常但做完灰度转换或 FFT输出图像黑乎乎一片数值范围严重超出预期。原因lunar_surface.tif 是 16 位深图像IMREAD_UNCHANGED 读进来是 uint16 类型数值范围 0-65535。后续卷积核高斯核的输出值都在几千上万的量级显示时按 0-255 线性映射大部分像素都超过 255显示成纯白或纯黑。解决读取时直接指定 cv2.IMREAD_GRAYSCALE或者读进来后先转 float32 再除以 65535 归一化到 0-1 区间。我在处理所有 tif 图像时都强制先打印 img.dtype 和 img.max()确认数据范围再走后续流程。从那以后这种翻车再没遇到过。5.2 不调用 fftshift频谱图能量全部堆积在四个角现象对 cameraman.tif 做 FFT 后直接取幅度显示发现图像四个角各有一个亮斑中间一片黑完全看不出频率分布规律。原因np.fft.fft2 输出的低频分量在矩阵四角这是 FFT 算法的自然布局。四角的亮斑就是 DC 分量中间区域对应高频分量数值相对太小线性显示下全被压暗。解决在取绝对值之前先执行 np.fft.fftshift把低频移动到中心。检查是否做对可以看中心区域幅度是否远大于其他区域。如果用了 fftshift 还是四角亮检查是不是在 ifftshift 之后又做了一次 fftshift两个 shift 叠加等于没做。5.3 频谱图像直接显示一片黑只有中心一个亮点现象fftshift 之后取 np.abs(f)直接 imshow整张图只有中心一点亮光周围全是黑色高频信息完全看不到。原因幅度谱的动态范围太大了。DC 分量能达到几千万高频细微纹理的幅度可能只有几百线性映射下所有非低频信息都被压缩到不可见。解决取幅度后做 np.log1p 压缩动态范围再用 cv2.normalize 归一化到 0-255。如果做了 log 之后还是偏暗检查是否用了 np.abs 而不是 np.sqrt 或者是否包含了 DC 分量参与归一化——DC 分量贡献了 log 变换后的主要亮度范围把它单独抑制掉也是一种常见做法。5.4 2 的幂次填充后在图像边缘出现规律性条纹现象把图像从 256x256 零填充到 512x512 再 FFT 做低通滤波重建后的图像边缘出现一圈圈明暗交替的条纹像水波纹。原因零填充等于在图像四周制造了从真实像素到 0 的阶跃跳变这个跳变在频域里对应额外的高频分量。如果再用理想矩形低通掩码滤波矩形截止在频域上等价于 sinc 函数的卷积在空间域表现为振铃波纹。两个因素叠加边缘条纹特别明显。解决处理前先把图像值减去均值再做 FFT从根源上削弱 DC 分量或者用高斯低通掩码替代理想矩形掩码高斯在频域平滑截止不会产生 sinc 振铃。输入里提到的 2 的整数次幂填充本身是为了 FFT 算法固定 radix-2 尺寸提升速度但填充方式最好用镜像填充而非零填充。5.5 逆变换后直接 uint8 强转重建图像出现大片黑白噪点现象ifft2 得到的结果直接调 astype(np.uint8)输出的图像出现很多随机的白色斑点边缘地方还会出现黑色条纹。原因ifft2 输出是复数存在浮点误差。即使理论上实部应该完全等于原图实际计算中虚部会有极小的残余值实部也可能出现负值或超过 255 的值。uint8 转换时负数映射成 255超过 255 的值回绕到 0 附近就产生了随机噪点。解决先取 np.real 丢弃虚部再用 cv2.normalize 把实部数值线性映射到 0-255最后才转 uint8。如果想评估重建误差不要直接比较 uint8 图像而是用 float32 的原图与重建图计算均方误差。我在对比滤波前后图像质量时固定这套流程避免把数值类型误差误判为算法缺陷。6. 把课程设计工程化ImageProcess.py 的分层写法与命令行参数化6.1 把读图、处理、保存拆成三个独立函数课程设计交上去的脚本如果只是把几十行代码堆在 notebook 里答辩时很难讲清楚。更实际的做法是把整个流程抽象成三个独立函数每个函数只做一件事主入口用 argparse 接收命令行参数这样既能处理单张图像也能批量处理整个目录。import argparse import cv2 import numpy as np def read_image(path, grayTrue): flag cv2.IMREAD_GRAYSCALE if gray else cv2.IMREAD_COLOR img cv2.imread(path, flag) if img.dtype np.uint16: img (img / 65535.0 * 255.0).astype(np.uint8) return img def gaussian_smooth(img, ksize5, sigma1.2): return cv2.GaussianBlur(img, (ksize, ksize), sigma) def frequency_spectrum(img): fshift np.fft.fftshift(np.fft.fft2(img.astype(np.float32))) magnitude np.log1p(np.abs(fshift)) return cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) if __name__ __main__: parser argparse.ArgumentParser(description数字图像处理课程设计) parser.add_argument(--input, requiredTrue, help输入图像路径) parser.add_argument(--output, defaultoutput.png, help输出图像路径) parser.add_argument(--mode, defaultgray, choices[gray, blur, fft], help处理模式灰度化/高斯平滑/频谱) args parser.parse_args() image read_image(args.input) if args.mode gray: result image elif args.mode blur: result gaussian_smooth(image) elif args.mode fft: result frequency_spectrum(image) cv2.imwrite(args.output, result) print(f已保存: {args.output})逻辑说明read_image 统一入口处理不同位深问题内部把 uint16 转成 8 位灰度避免后面每个函数都要判断类型。gaussian_smooth 和 frequency_spectrum 各自负责一个处理模式主流程只根据 mode 做分发。参数说明argparse 的 choices 限制了 mode 只能是 gray、blur、fft 三个值输入其他值会直接报错而不是静默进入错误分支。这种设计在答辩时要展示哪个模块直接改 --mode 参数就行不用改代码。6.2 批量跑完整个测试集用文件名后缀区分处理结果课程设计最花时间的往往不是写算法而是验证算法在整套测试图像上的表现。手动一张张跑完再手动保存效率太低。把 ImageProcess.py 扩展成批量模式用 glob 遍历所有图像按模式生成带后缀的输出文件名。import glob import os import argparse import cv2 import numpy as np def batch_process(input_dir, output_dir): os.makedirs(output_dir, exist_okTrue) image_paths glob.glob(os.path.join(input_dir, *.tif)) \ glob.glob(os.path.join(input_dir, *.png)) \ glob.glob(os.path.join(input_dir, *.tiff)) for path in image_paths: base os.path.splitext(os.path.basename(path))[0] image read_image(path) if args.mode gray: cv2.imwrite(os.path.join(output_dir, f{base}_gray.png), image) elif args.mode blur: blurred gaussian_smooth(image) cv2.imwrite(os.path.join(output_dir, f{base}_blur.png), blurred) elif args.mode fft: spectrum frequency_spectrum(image) cv2.imwrite(os.path.join(output_dir, f{base}_fft.png), spectrum) print(f批量处理完成共 {len(image_paths)} 张图像)逻辑说明glob 同时匹配 tif、png、tiff 三种常见格式splitext 取文件名主体输出文件名加后缀避免覆盖原图。exist_okTrue 允许重复执行而不报错多次调试时不用手动删输出目录。参数说明这个批量脚本直接把第 6.1 节的三个函数拿过来复用体现了函数拆分的好处。如果后续要增加新处理模式只需要新写一个处理函数并在 if 分支里加一行调用。实际使用时先把输入目录里混放的各类图像挑出来避免把 README 或 LICENSE 文件也当作图像读进去。这套工程化脚本我在课程设计答辩前反复用过多次。从那以后我每次拿到新的图像数据集都会强制走一遍「先确认 dtype 和通道顺序再统一读图入口最后批处理出结果」的流程。把 ImageProcess.py 里的三个函数当作地基后续加任何新算法都只是往上垒砖不会再因为读图类型问题推倒重来。希望这套流程和踩坑记录能帮你在同样的路上少走几步。本文还有配套的精品资源点击获取