拓冰建站拓冰建站
首页 / 资讯中心 / 正文

计算机视觉入门:图像增强与分割的Python源码复现实践

简介这是一个面向计算机视觉初学者的Python源码复现合集聚焦图像分割与图像增强两大经典方向适合正在学习数字图像处理、OpenCV或准备课程设计的学生。资源覆盖迭代阈值分割、最大类间方差法、基于最大熵的阈值分割、马尔可夫遍历等分割算法以及单尺度SSR、多尺度MSR、直方图均衡、自适应直方图均衡等增强方法每个算法均提供自写实现和参考程序并配有原理说明文档与对比实验报告。压缩包共28个文件其中包含17个Python脚本、4张JPG测试图、3个Markdown说明、2个TXT辅助文档和1份PDF研究报告整体仅2.46MB目录按图像分割与图像增强分为两大模块便于按需检索。目前已有584人学习下载。读者可获得带注释的完整源码、算法思路讲解、实验对比结果以及可直接运行的测试图片能通过动手复现深入理解阈值分割、直方图均衡等核心原理是入门计算机视觉算法实践的高性价比资源。1. 计算机视觉入门项目在学什么一个 zip 包里能带你走到哪你拿到的这个标题本质上是一份「带注释的源码复现练习册」把图像分割、图像增强这两类最典型的图像处理算法用 Python 重新实现一遍并且把注释写到能看懂的水平。它的价值不在于某个算法有多新而在于它是给入门者设计的——你能在这个包里清楚看到一张图从读入、预处理、算法处理到输出结果的完整链路。计算机视觉入门最怕的不是数学而是「代码能跑但看不懂为什么这样写」这个包解决的就是这个问题。适合正在做课程作业、准备毕设开题或者想从调包侠变成真正会改算法的人。接下来我按自己跑这类项目的习惯把环境、源码复现、参数调优和踩坑记录完整拆一遍。2. 把 Python 环境先打通版本选择、依赖库分工和项目目录结构2.1 为什么推荐 Python 3.9 到 3.10而不是最新版做图像处理源码复现最常见翻车点不是算法而是环境。我一般建议装 Python 3.9 或 3.10不要追最新版本。原因是 OpenCV、PyTorch 这些库对 Python 版本的官方支持要慢半拍你装完最新的 Python 3.13大概率会在pip install torch时看到「找不到匹配的发行版」之类报错这时候你还没开始跑图像分割就已经消耗掉两小时耐心。推荐用虚拟环境隔离这个项目不要直接装进系统 Python。常见做法是# 创建虚拟环境cv_proj 是环境名可以随便改 python -m venv cv_proj # 激活环境 # Windows: cv_proj\Scripts\activate # Linux / macOS: source cv_proj/bin/activate # 安装依赖 pip install numpy opencv-python scikit-image matplotlib pywavelets逻辑说明venv是 Python 自带的虚拟环境工具不需要额外安装。激活后终端提示符前面会出现(cv_proj)这一步的作用是把当前项目所有第三方库隔离在这个文件夹里你后面随便折腾 OpenCV 版本都不会影响系统里其他 Python 项目。参数说明如果你是在国内网络环境pip 下载速度不理想可以在 pip 命令后面加-i https://pypi.tuna.tsinghua.edu.cn/simple临时换用清华镜像源这个参数只在当前命令生效不会写进全局配置。2.2 图像处理依赖库的分工OpenCV、scikit-image 和 pywavelets 各管什么很多刚入门的朋友一上来就把 OpenCV 当成万能工具箱遇到小波变换、质量评估也要硬用 cv2 实现结果代码又长又容易出错。我习惯按库的分工来组织源码库负责的算法模块在这个项目里的典型用途OpenCVcv2图像读写、颜色空间、直方图均衡化、阈值分割、形态学、边缘检测90% 的基础图像处理操作numpy数组运算、矩阵操作、数据类型转换、归一化所有算法实现的数据结构底座scikit-image高级分割算法、图像恢复、质量评估指标PSNR、SSIM 计算SLIC 超像素分割pywavelets小波变换、系数处理小波域图像增强matplotlib可视化、对比图展示算法前后对比、保存效果图这个分工会影响你阅读源码时的思路看到cv2.threshold知道是哪种算法看到pywt.wavedec2知道是小波分解。这个包里算法文件一般会按enhancement/和segmentation/两个目录划分每个算法一个独立.py文件文件开头会有一段说明注释写清楚「输入什么、输出什么、核心参数是哪个」。这样做的好处是方便你单独运行某一个算法文件来验证效果而不必把整个项目的 main.py 跑一遍。2.3 环境验证写一个最小脚本检查 OpenCV 是否正常工作安装完依赖不要急着跑算法。先写一个最简单的脚本验证图像读写链路通畅这一步能过滤掉大部分环境问题# check_env.py import numpy as np import cv2 import matplotlib.pyplot as plt # 用 cv2 读一张图注意读进来是 BGR 顺序 img cv2.imread(data/demo.jpg) print(图像 shape:, img.shape) # 期望输出 (H, W, 3)不是 (3, H, W) # 转成 RGB 再显示否则颜色会偏蓝偏红 rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(rgb) plt.axis(off) plt.savefig(output/check_env.png, dpi150, bbox_inchestight) print(环境检查通过输出已保存到 output/check_env.png)逻辑说明cv2.imread读出来的数组形状是(height, width, channels)这是 OpenCV 的约定。cv2.cvtColor把 BGR 转成 RGB是因为 matplotlib 的imshow默认按 RGB 解释数组。如果不做这一步你后面所有图的颜色都会是错乱的这是图像处理入门第一个经典的肉眼可见的坑。参数说明dpi150控制保存图片清晰度bbox_inchestight会裁掉多余白边这两个参数在生成对比图时很常用。如果脚本报错ModuleNotFoundError: No module named cv2说明 OpenCV 没装上重跑 pip install 即可。如果报错AttributeError: module cv2 has no attribute imread大概率是你的 python 文件叫cv2.py或者当前目录下有同名文件把文件名改掉就行。3. 图像增强源码复现直方图均衡化、Retinex 与小波变换增强的取舍3.1 全局直方图均衡化与 CLAHE一个clipLimit参数决定的两种命运直方图均衡化是图像增强里最基础也最常被过度使用的算法。它的原理一句话把像素灰度分布从集中区域拉伸到整个灰度范围让暗的更暗、亮的更亮对比度自然提升。这个包里的源码复现通常会同时给出全局均衡化和 CLAHE对比度受限的自适应直方图均衡化两个版本因为它们的适用场景差别很大。import cv2 # 读成灰度图IMREAD_GRAYSCALE 对应的数值是 0 gray cv2.imread(data/input.jpg, cv2.IMREAD_GRAYSCALE) # 版本一全局直方图均衡化简单但容易过曝 he cv2.equalizeHist(gray) # 版本二CLAHE分块做均衡化并限制对比度 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) cl clahe.apply(gray) cv2.imwrite(output/he.jpg, he) cv2.imwrite(output/clahe.jpg, cl)逻辑说明全局直方图均衡化是把整张图的灰度直方图统一拉伸如果图像里有一大片明亮天空暗部细节会被严重压缩。CLAHE 把图像分成 8x8 的小块每个块内部单独做均衡化然后用双线性插值消除块与块之间的边界痕迹。clipLimit是 CLAHE 最核心的参数它限制每个灰度直方图桶的高度防止某个区域内对比度被拉得过高而产生噪声。参数说明clipLimit2.0是保守值处理夜景或低照度图时可以调到 3.0 到 4.0但注意噪声会同步放大tileGridSize越小局部细节增强越明显但计算量增大且可能出现块状伪影。3.2 Retinex 低照度图像增强光的估计与反射分量提取低照度图像增强是热门的落地场景比如夜间监控、暗光摄影。Retinex 是所有这类方法的理论基础核心思想人眼看到的图像 入射光照分量 × 物体反射分量。增强目标就是把光照分量去掉或压平保留反射分量。这个包的源码复现里通常会有单尺度 RetinexSSR和多尺度加权版MSR。import numpy as np import cv2 def single_scale_retinex(img, sigma80): # 转浮点并加 1防止 log(0) 报错 img img.astype(np.float32) 1.0 # 高斯模糊模拟入射光照的估计sigma 控制光照平滑程度 blur cv2.GaussianBlur(img, (0, 0), sigmaXsigma) # 对数域相减得到反射分量 retinex np.log(img) - np.log(blur) return retinex def normalize_8u(img): # 线性拉伸到 [0, 255]转回 uint8 才能正常显示 img cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX) return np.clip(img, 0, 255).astype(np.uint8) # 多尺度 Retinex多个 sigma 的结果加权平均兼顾局部细节和整体色调 def multi_scale_retinex(img, sigmas(80, 160, 320), weights(0.3, 0.4, 0.3)): result np.zeros_like(img, dtypenp.float32) for sigma, w in zip(sigmas, weights): result w * single_scale_retinex(img, sigma) return normalize_8u(result)逻辑说明sigma80表示高斯模糊的半径尺度sigma 越小估计出的光照越精细但容易把物体本身的纹理也当成光照抹掉sigma 越大光照估计越平滑增强结果更自然但暗部提亮不明显。多尺度 Retinex 就是把不同 sigma 的结果按权重叠加兼顾两种效果。参数说明权重数组weights加起来必须等于 1否则整体亮度会偏移这是新手最容易忽略的点。另外注意整个过程是在对数域做的因为对数可以把乘性光照变成加性的计算更方便。3.3 小波变换图像增强分频率处理高频去噪与低频拉伸互不干扰小波变换是比直方图均衡化理论门槛更高的一种增强手段常见的落地方式是把图像分解成低频近似分量和高频细节分量低频做对比度拉伸高频做阈值去噪再重建回一张图。它的优势是能在提升对比度的同时压制噪声而 CLAHE 和 Retinex 做不到这一点。import numpy as np import pywt def wavelet_enhance(gray, level2, denoise_thresh10): # 2 层小波分解返回低频 每层的三个高频方向水平/垂直/对角 coeffs pywt.wavedec2(gray, db2, levellevel) cA, *details coeffs # 低频分量整体拉伸提升全局对比度 cA cA * 1.1 5 # 高频分量做软阈值去噪避免增强后噪声被放大 processed_details [] for detail in details: processed tuple(pywt.threshold(d, denoise_thresh, soft) for d in detail) processed_details.append(processed) # 重构回原尺寸 rec pywt.waverec2([cA] processed_details, db2) return np.clip(rec, 0, 255).astype(np.uint8)逻辑说明wavedec2的返回值里coeffs[0]是低频近似分量代表图像的主体亮度结构details里是三层高频细节系数。这里对低频做* 1.1 5相当于亮度拉伸对高频做软阈值去噪。参数说明level2表示分解层数层数越多频率划分越细但层数超过 3 之后重构图像容易失真一般取 2 到 3 层。denoise_thresh10是高频系数的阈值小于该值的系数被置零或收缩调大去噪更强但图像会变糊调小保留更多纹理但噪声也保留。db2是小波基函数常用还有haar和sym4haar最轻量但容易出现方块效应普通图像用db2或sym4比较均衡。3.4 三个增强算法的效果对比与选择思路算法适用场景主要风险核心调节参数表现力直方图均衡化灰度分布集中、对比度低过曝、色彩失真无CLAHE 看 clipLimit单张图效果好但一次性处理长序列视频容易闪烁Retinex低照度、夜间、雾天光晕、颜色偏灰sigma、多尺度权重有物理意义适合监控视频增强小波变换增强纹理类图像、需要抑制噪声的场景伪影、细节丢失level、去噪阈值频率域分离能把去噪和增强同时做我一般会建议先跑直方图均衡化看效果下限再做 Retinex 对比亮度恢复能力最后用小波变换处理噪声明显的图。这个顺序也是这个包里增强算法文件组织的顺序按顺序跑一遍能直观感受到每种算法的行为差异。4. 图像分割源码复现从 Otsu 阈值到区域生长和 Unet 推理4.1 传统分割第一步Otsu 阈值分割与形态学修正图像分割的目标是把图像分成若干个有意义的区域。最简单的入门算法是阈值分割Otsu 是其中最经典的自动化方法它自动计算一个灰度阈值把像素分成前景和背景两类依据是让类间方差最大。广告牌这类背景简单、目标与背景灰度差异大的场景Otsu 效果就很理想。import cv2 import numpy as np gray cv2.imread(data/input.jpg, cv2.IMREAD_GRAYSCALE) # Otsu 自动计算阈值并二值化 thresh_val, mask cv2.threshold( gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU ) print(Otsu 自动选出的灰度阈值:, thresh_val) # 预期输出类似 117不同图像差异很大 # 形态学修正先开运算去小白点再闭运算补内部空洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations1) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations1)逻辑说明cv2.threshold第一个参数是灰度图第二个参数传 0 表示阈值由 Otsu 自动计算第三个参数 255 是目标像素值。加了cv2.THRESH_OTSU标志后函数会忽略传入阈值自动计算最佳阈值并返回。形态学开运算是先腐蚀后膨胀去掉前景区域外部的孤立白点闭运算是先膨胀后腐蚀填补目标内部的黑色孔洞。参数说明(5, 5)是椭圆结构元素的尺寸目标物体越大结构元素适当调大但对细小目标不能超过目标尺寸否则会把目标直接抹平。iterations1是执行次数加大意味着更强的修正效果但也会让轮廓变钝。4.2 区域生长算法手写复现种子点与相似性准则阈值分割只考虑像素本身的灰度值不考虑空间邻接关系。区域生长是另一种经典思路从种子点出发逐步把周围相似像素纳入当前区域。这个算法很适合作入门源码复现因为它逻辑直观但实现细节上有些讲究。import numpy as np from collections import deque def region_growing(gray, seed, threshold20): h, w gray.shape mask np.zeros((h, w), np.uint8) queue deque([seed]) visited np.zeros((h, w), dtypebool) visited[seed] True seed_val int(gray[seed]) while queue: x, y queue.popleft() # 相似性准则当前像素与种子点灰度差在阈值内 if abs(int(gray[x, y]) - seed_val) threshold: continue mask[x, y] 255 # 四邻域扩展检查上下左右四个方向 for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): nx, ny x dx, y dy if 0 nx h and 0 ny w and not visited[nx, ny]: visited[nx, ny] True queue.append((nx, ny)) return mask # 使用示例手动指定种子点或选图像中最亮的像素点 seed (100, 100) result_mask region_growing(gray, seed, threshold25)逻辑说明deque是双端队列这里作为 BFS 的待访问列表。visited数组防止同一个像素被反复加入队列这是血泪经验——不加 visited 的死循环能把程序跑挂。相似性准则用的是「与种子点灰度差绝对值小于阈值」而不是与当前像素比较两种策略会造成分割结果的明显差异与种子点比较得到的是灰度相近的连通区域与当前像素比较则会沿着灰度渐变方向一直生长容易溢出到背景。参数说明threshold20表示灰度差容忍度目标内部灰度不均匀时调大背景与目标边界清晰时可以调小到 10 左右。四邻域比八邻域更保守边界更平滑但遇到对角线连接的区域会断裂需要根据目标形状选择。4.3 深度学习分割的源码复现Unet 的预处理、推理和输出解析如果这个包里包含深度学习的图像分割部分那大概率是 Unet 结构医学图像分割是最常见的训练场景。对入门者来说不需要从零手写训练循环关键是弄懂模型的输入输出格式输入是一张归一化后的(1, 3, H, W)张量输出是(1, num_classes, H, W)的 logits通过argmax得到每个像素的类别编号。import numpy as np import cv2 import torch # 假设已加载训练好的模型权重这里只展示推理流程 # model Unet(in_channels3, num_classes2) # 实际结构在源码中定义 # state_dict torch.load(weights/unet_2cls.pth, map_locationcpu) # model.load_state_dict(state_dict) # model.eval() def predict_mask(model, rgb_image, size(256, 256)): # 统一输入尺寸Unet 的下采样要求尺寸是 2 的倍数 resized cv2.resize(rgb_image, size) # 转成 float32 并归一化到 [0, 1] tensor resized.astype(np.float32) / 255.0 # HWC 转 CHW 并增加 batch 维度 tensor tensor.transpose(2, 0, 1)[None, ...] with torch.no_grad(): logits model(torch.from_numpy(tensor)) # logits 形状: (1, num_classes, H, W) # argmax 在类别维度取最大值的索引得到单通道 mask mask torch.argmax(logits, dim1).squeeze(0).numpy() return mask.astype(np.uint8)逻辑说明resize到 256x256 是为了满足模型固定输入尺寸的要求Unet 每次下采样让特征图尺寸减半所以输入尺寸必须能被 2 整除多次。归一化到[0, 1]是对应训练时的预处理方式如果训练用的归一化是均值方差归一化这里也要保持一致否则模型输出会异常。torch.no_grad()在推理时关闭梯度计算节省显存并加速。参数说明map_locationcpu用于在无 GPU 环境加载模型权重如果你用 GPU 可以去掉这个参数num_classes2表示前景/背景二分类如果源码里是多类别分割比如器官分割有多类argmax输出的索引就是类别 ID可视化时需要乘以 255 或映射到调色板才能显示。5. 图像处理的常见避坑与排查从读图到算指标的四条血泪经验5.1 读图读出来是 None或颜色全偏色现象cv2.imread读取本地图片代码不报错但下一步操作时报错提示NoneType object has no attribute shape或者读出来的图用 matplotlib 显示后蓝天变黄、绿叶变紫。原因前一种大概率是文件路径包含中文或特殊字符OpenCV 底层用的是 C 的imread对中文路径支持很差读取失败后返回None而 Python 不会因此抛出异常。后一种是你忘了 OpenCV 读进来是 BGR 通道顺序直接用plt.imshow显示就会色彩错乱。解决中文路径用cv2.imdecode配合np.fromfile读取显示前用cvtColor转 RGB。这是图像处理入门第一个玄学问题排查优先级最高。import cv2 import numpy as np # 路径含中文时的读取方案 def imread_chinese(path, flagscv2.IMREAD_COLOR): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, flags)5.2 uint8 运算溢出导致图像出现雪花噪点现象对图像做减法或乘法后得到的结果图上有大量黑白刺眼的噪点甚至出现负值区域变白的情况。原因图像默认是uint8类型取值范围 0 到 255。做img1 - img2时如果结果为负数uint8会发生环绕比如 0 减 1 变成 255。这就是计算时不注意数据类型导致的经典翻车。解决运算前把图像转成float32或float64所有算术操作在浮点类型下进行最后用cv2.normalize拉伸回 0 到 255 并转回uint8。凡是涉及np.log、高斯模糊差值、小波变换重建这类操作都要遵循这个习惯。5.3 小波变换重建后图像出现格状伪影或整体偏灰现象用pywt.waverec2重建后的图像放大看有明显的方块纹理或者整张图对比度极低、灰蒙蒙一片。原因格状伪影通常是小波基选择不当haar小波会产生明显的块状效应整体偏灰则是因为重建结果的范围不是 0 到 255可能落在 -50 到 300 之间直接astype(np.uint8)会发生截断和环绕。解决细节纹理较多的图换用db2或sym4小波基重建后别着急转类型先做一次线性归一化我习惯用cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)把最小值映射到 0、最大值映射到 255再去clip和转类型。调整denoise_thresh时注意观察细节区阈值太大会导致纹理被磨平。5.4 Otsu 阈值分割对灰度直方图不呈双峰分布的图几乎失效现象对一张背景复杂的图跑 Otsu得到的 mask 里目标区域和背景区域混在一起没有任何分割效果但代码逻辑看起来完全正常。原因Otsu 的理论前提是图像灰度直方图呈双峰分布一个峰是背景、一个峰是目标。如果场景光照不均、目标颜色与背景接近直方图是单峰或平坦分布Otsu 算出来的「最优阈值」只是在数学上让类间方差最大并没有语义含义。解决两条路。一是先做预处理对光照不均的图先跑 CLAHE 增强再直方图均衡化让灰度分布拉开二是放弃全局阈值改用自适应阈值cv2.adaptiveThreshold它对光照变化更鲁棒。做计算机视觉入门项目遇到算法不 work 时先看输入数据的直方图长什么样这个排查习惯比调参有用得多。6. 把增强和分割串成一条流水线评估指标与调参习惯前面的增强和分割是两个独立模块实际做项目时通常要串起来低照度图像先增强再分割目标区域。我一般会写一个组合推理脚本顺带把评估指标也算出来这样才能量化每一步改进的效果而不是凭肉眼感觉「好像变好了」。import cv2 import numpy as np # 完整流水线低照度图 - Retinex 增强 - Otsu 分割 def enhance_and_segment(image_path): img cv2.imread(image_path) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 用前面封装好的多尺度 Retinex 做增强 enhanced multi_scale_retinex(rgb) gray cv2.cvtColor(enhanced, cv2.COLOR_RGB2GRAY) # Otsu 分割增强后直方图拉开阈值更稳定 _, mask cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return enhanced, mask # 评估分割质量ground_truth 是人工标注的 mask def compute_iou(pred_mask, gt_mask): pred_mask pred_mask 0 gt_mask gt_mask 0 intersection np.logical_and(pred_mask, gt_mask).sum() union np.logical_or(pred_mask, gt_mask).sum() return intersection / (union 1e-6) # 加小量防止除零评估指标方面图像增强用 PSNR 和 SSIM两者都需要参考图适合有标准答案的复原实验没有参考图时可以用 NIQE 这类无参考指标分数越低越好。分割统一用 IoU 或 DiceIoU 偏向评估边界准确度Dice 对小目标更敏感。这里的重点是所有指标都要跑出数值、记录到一张表里不要凭记忆比较。最后分享一个我做源码复现项目的习惯每次只改一个参数跑完立即保存输出图并记录指标形成类似「sigma80, IoU0.82sigma160, IoU0.85」的对照表同时写下一行注释说明这次改动的动机和效果。这样看起来慢但回头写报告或改代码时每张图每个参数都有据可查。复现这个代码系列时强烈建议你也在每个算法文件的main里加上print输出关键变量的 shape 和取值范围——很多看似玄学的算法异常本质上都是数据类型和数组形状在作祟。认真走完一遍增强和分割的复现你对计算机视觉的感知会从「调函数」变成「理解一块像素数据如何被一步步变换成结果」。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门