☰
图像分割与图像增强实战:从Otsu、U-Net到CLAHE、Retinex源码复现
2026/10/9 1:01:24 网站建设 项目流程

简介:这份资源面向计算机视觉入门者、正在做毕设或课程设计的学生,聚焦图像分割与图像增强两大经典方向,提供可直接运行的Python复现代码与配套说明。包内共28个文件,以17个py源码为主,辅以jpg、png测试图像、md项目说明、txt辅助文档及pdf实验报告,压缩包约2.46MB,结构按图像分割、图像增强、参考程序与自写代码分目录组织。图像分割部分覆盖最大类间方差法(OTSU)、最大熵阈值分割、迭代阈值分割、Canny边缘检测及马尔可夫随机场,其中带zixie后缀的文件为依据论文复现的实现,不带后缀的则调用OpenCV库函数,便于在效果与耗时上做对比;图像增强部分包含直方图均衡、CLAHE、单尺度SSR与多尺度MSR等算法。资源还附有算法原理与实现效果说明,可作为毕设、期末大作业直接使用,也适合借鉴排错思路与目录组织方式。目前已有1160人学习下载。

1. 从一份图像处理源码包说起:图像分割与图像增强到底怎么落地

很多人第一次接触计算机视觉,都是从跑通一份图像分割、图像增强的 Python 源码开始的。你手里可能正好有这样一份压缩包:里面是多个图像处理算法的复现,带详细注释和项目说明。问题在于,直接解压、python xxx.py跑一遍,看到几张输出图,然后呢?大部分人卡在这里——不知道每个算法解决什么问题,不知道参数怎么调,换一张自己的图就翻车。

这份源码包的价值不在于「跑通」,而在于它是一套可拆解的学习骨架。图像分割负责把目标从背景里抠出来,图像增强负责让暗的变亮、糊的变清、对比度低的变分明。两者经常配合使用:先增强再分割,或者先分割再对目标区域做增强。适合刚入门计算机视觉、想通过复现建立手感的人,也适合需要快速搭一个图像处理 pipeline 的工程师。下面我按「先立住原理、再动手复现、最后避坑」的顺序,把这份源码包里最值得吃透的几个算法讲清楚。

2. 图像分割算法复现:从阈值法到 U-Net 的最小可跑路径

2.1 分割算法的选型逻辑:什么时候用传统方法,什么时候上深度学习

图像分割算法大致分两代。第一代是传统方法:全局阈值、Otsu 自适应阈值、Canny 边缘检测加轮廓提取、分水岭算法。这类方法不需要训练数据,几行代码就能跑,适合背景干净、目标与背景灰度差异明显的场景,比如文档扫描里的文字区域提取、工业零件计数。第二代是基于深度学习的方法,代表是 U-Net 及其变体。U-Net 的编码器-解码器结构加跳跃连接,能在小样本医学图像上取得很好的效果,后来被广泛用于遥感、工业缺陷检测。

选型的判断标准很简单:如果你的图像背景可控、目标形状规则、光照稳定,先用传统方法试,成本低、可解释性强。如果背景复杂、目标边界模糊、光照变化大,传统方法调参调到怀疑人生也搞不定,那就上 U-Net。源码包里通常两种都有,建议先跑传统方法建立直觉,再跑 U-Net 理解端到端分割在做什么。

2.2 用 OpenCV 跑通 Otsu 阈值分割与分水岭算法

先看传统方法的最小实现。下面这段代码实现了 Otsu 全局阈值分割和基于距离变换的分水岭分割,适合处理细胞、颗粒这类粘连目标。

import cv2 import numpy as np # 读取图像并转灰度 img = cv2.imread('input.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Otsu 全局阈值分割:自动计算最佳阈值 # 参数 0, 255 是阈值范围,cv2.THRESH_OTSU 让算法自动找阈值 ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) print(f'Otsu 自动计算的阈值: {ret}') # 形态学去噪:先开运算去掉小噪点,再膨胀填补目标内部空洞 kernel = np.ones((3, 3), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) sure_bg = cv2.dilate(opening, kernel, iterations=3) # 距离变换:计算每个前景像素到最近背景像素的距离 dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5) # 取距离变换的 0.7 倍作为确定前景阈值,这个系数需要根据目标大小调整 ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # 找到不确定区域(边界) sure_fg = np.uint8(sure_fg) unknown = cv2.subtract(sure_bg, sure_fg) # 连通域标记,为分水岭准备标记图 ret, markers = cv2.connectedComponents(sure_fg) markers = markers + 1 # 背景从 1 开始,避免 0 被当作未知区域 markers[unknown == 255] = 0 # 未知区域标记为 0 # 分水岭算法 markers = cv2.watershed(img, markers) img[markers == -1] = [0, 0, 255] # 边界标红 cv2.imwrite('otsu_result.jpg', thresh) cv2.imwrite('watershed_result.jpg', img)

这段代码的关键参数有三个。第一个是形态学核大小(3, 3),如果你的目标很小,核要缩小到(2, 2),否则目标会被腐蚀掉。第二个是距离变换阈值系数0.7,目标越圆、粘连越少,这个值可以调高到0.8;目标形状不规则就降到0.5左右。第三个是connectedComponents的标记偏移,markers + 1是为了让背景标记为 1 而不是 0,因为分水岭把 0 当作未知区域。跑完之后看watershed_result.jpg,红色边界线如果断断续续,说明sure_fg提取不完整,回去调距离变换系数。

2.3 U-Net 分割的最小训练与推理脚本

传统方法搞不定的场景,上 U-Net。下面是一个极简的 U-Net 实现和训练循环,输入图像 resize 到 256×256,输出单通道分割掩码。

import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch=3, out_ch=1): super().__init__() # 编码器:四次下采样 self.enc1 = DoubleConv(in_ch, 64) self.enc2 = DoubleConv(64, 128) self.enc3 = DoubleConv(128, 256) self.enc4 = DoubleConv(256, 512) self.pool = nn.MaxPool2d(2) # 瓶颈层 self.bottleneck = DoubleConv(512, 1024) # 解码器:四次上采样,跳跃连接拼接 self.up4 = nn.ConvTranspose2d(1024, 512, 2, stride=2) self.dec4 = DoubleConv(1024, 512) self.up3 = nn.ConvTranspose2d(512, 256, 2, stride=2) self.dec3 = DoubleConv(512, 256) self.up2 = nn.ConvTranspose2d(256, 128, 2, stride=2) self.dec2 = DoubleConv(256, 128) self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec1 = DoubleConv(128, 64) self.out = nn.Conv2d(64, out_ch, 1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) e4 = self.enc4(self.pool(e3)) b = self.bottleneck(self.pool(e4)) d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1)) d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1)) return torch.sigmoid(self.out(d1)) # 训练循环核心 model = UNet().cuda() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) criterion = nn.BCELoss() for epoch in range(50): for img, mask in dataloader: # img: [B,3,256,256], mask: [B,1,256,256] img, mask = img.cuda(), mask.cuda() pred = model(img) loss = criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() print(f'Epoch {epoch}, Loss: {loss.item():.4f}')

U-Net 的跳跃连接是它区别于普通自编码器的核心。编码器每次下采样丢失的空间信息,通过跳跃连接直接拼接到解码器对应层,让分割边界更精细。训练时学习率1e-4是常用起点,如果 loss 震荡就降到1e-5。BCELoss适合二分类分割,多分类换成CrossEntropyLoss并把输出通道改成类别数。推理时记得把输出经过sigmoid后以0.5为阈值二值化,这个阈值可以根据对漏检和误检的容忍度调整。

3. 图像增强算法复现:直方图均衡、小波变换与 Retinex 的实战参数

3.1 直方图均衡化与 CLAHE:全局和局部的取舍

直方图均衡化是最基础的图像增强方法,把原始图像的灰度直方图拉平,让暗部细节显现。但它有个致命问题:全局处理会放大背景噪声,尤其是医学图像和夜间图像。CLAHE(限制对比度自适应直方图均衡)通过分块处理加对比度限制解决了这个问题。

import cv2 import numpy as np img = cv2.imread('dark.jpg') # 转 LAB 空间,只对 L 通道做增强,避免颜色偏移 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) # 全局直方图均衡化 l_eq = cv2.equalizeHist(l) # CLAHE:clipLimit 控制对比度限制,tileGridSize 控制分块大小 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) l_clahe = clahe.apply(l) # 合并通道并转回 BGR result_eq = cv2.cvtColor(cv2.merge([l_eq, a, b]), cv2.COLOR_LAB2BGR) result_clahe = cv2.cvtColor(cv2.merge([l_clahe, a, b]), cv2.COLOR_LAB2BGR) cv2.imwrite('eq_result.jpg', result_eq) cv2.imwrite('clahe_result.jpg', result_clahe)

clipLimit是 CLAHE 最关键的参数,默认2.0适合大多数场景。调高到4.0增强效果更猛,但噪声也会被放大;调到1.0效果温和,适合医学图像。tileGridSize决定分块粒度,(8, 8)是常用值,图像分辨率高就改成(16, 16),否则块太小会导致块间过渡不自然。在 LAB 空间只处理 L 通道是血泪经验,直接在 RGB 上做均衡化会让颜色严重偏移。

3.2 小波变换图像增强的 Python 实现与分解层数选择

小波变换增强的思路是把图像分解成不同频率的子带,对高频子带(细节)做非线性增强,对低频子带(概貌)做适度拉伸,再重构回去。这种方法在保留边缘的同时抑制噪声,比直方图均衡化更精细。

import pywt import cv2 import numpy as np img = cv2.imread('input.jpg', 0).astype(np.float32) # 小波分解:选择 sym4 小波,分解 3 层 wavelet = 'sym4' level = 3 coeffs = pywt.wavedec2(img, wavelet, level=level) # coeffs[0] 是低频概貌,coeffs[1:] 是各层的高频细节 (水平, 垂直, 对角) # 对高频细节做增益增强,增益系数根据层数递减 gain = [1.0, 1.5, 2.0, 2.5] # 索引 0 对应低频,1-3 对应各层高频 enhanced_coeffs = [coeffs[0] * gain[0]] for i, detail in enumerate(coeffs[1:], start=1): # 每层三个方向:水平、垂直、对角 enhanced_detail = tuple(d * gain[i] for d in detail) enhanced_coeffs.append(enhanced_detail) # 小波重构 enhanced = pywt.waverec2(enhanced_coeffs, wavelet) enhanced = np.clip(enhanced, 0, 255).astype(np.uint8) cv2.imwrite('wavelet_enhanced.jpg', enhanced)

小波基的选择影响很大。sym4对称性好,适合自然图像;db4更常用但相位非线性;haar最简单但细节保留差。分解层数level=3是经验值,图像越大层数可以越多,但超过 4 层后低频信息过于抽象,增强效果反而下降。增益系数按层递减是因为高层高频主要是噪声,增益太大会把噪声也放大。如果增强后图像出现明显振铃效应,说明增益系数太大,把2.5降到1.8试试。

3.3 Retinex 多尺度增强:SSR 与 MSR 的参数对比

Retinex 理论认为图像由光照分量和反射分量组成,增强的目标是估计并去除光照不均。单尺度 Retinex(SSR)用高斯滤波估计光照,多尺度 Retinex(MSR)用多个高斯核加权。

import cv2 import numpy as np def single_scale_retinex(img, sigma): # 高斯模糊估计光照分量 blur = cv2.GaussianBlur(img, (0, 0), sigma) # 对数域相减得到反射分量 retinex = np.log10(img + 1.0) - np.log10(blur + 1.0) return retinex def multi_scale_retinex(img, sigmas=[15, 80, 250]): # 多尺度加权融合,权重通常取均值 retinex = np.zeros_like(img, dtype=np.float32) for sigma in sigmas: retinex += single_scale_retinex(img, sigma) retinex /= len(sigmas) # 归一化到 0-255 retinex = cv2.normalize(retinex, None, 0, 255, cv2.NORM_MINMAX) return retinex.astype(np.uint8) img = cv2.imread('input.jpg').astype(np.float32) + 1.0 # 分通道处理再合并 b, g, r = cv2.split(img) msr = cv2.merge([multi_scale_retinex(b), multi_scale_retinex(g), multi_scale_retinex(r)]) cv2.imwrite('msr_result.jpg', msr)

MSR 的三个高斯核[15, 80, 250]分别对应小、中、大尺度。小尺度增强局部细节,大尺度校正全局光照。如果图像整体偏暗,把大尺度核从250调到400;如果细节不够清晰,把小尺度核从15降到8。Retinex 处理后图像容易发灰,可以在归一化后做一次轻微的 gamma 校正,gamma 值取0.8到1.2之间。

4. 避坑与排查:图像分割和增强复现中最容易翻车的 5 个点

4.1 现象:Otsu 阈值分割结果全黑或全白

原因通常是图像本身对比度极低,或者前景背景像素比例严重失衡。Otsu 假设直方图是双峰的,如果图像只有一个峰,自动计算的阈值会落在极端位置。解决方法是先做一次 CLAHE 增强再跑 Otsu,或者改用自适应阈值cv2.adaptiveThreshold,块大小取图像宽度的1/8左右,常数 C 取2到10之间。

4.2 现象:U-Net 训练 loss 不下降,输出全是同一类

先检查数据标签是不是全 0 或全 1。二分类分割里如果正样本占比不到 5%,BCELoss会被负样本主导,模型学会全预测背景。解决办法是换DiceLoss或BCEWithLogitsLoss加pos_weight参数。另一个常见原因是输入图像没有归一化到[0, 1],像素值在0-255之间会导致梯度爆炸。在ToTensor()之后加Normalize(mean=[0.5]*3, std=[0.5]*3)。

4.3 现象:小波增强后图像出现网格状伪影

这是小波分解层数过多或增益系数在层间跳变太大导致的。重构时各层系数不连续会在空间域表现为网格。把level从4降到3,增益系数改成平滑过渡,比如[1.0, 1.3, 1.6, 1.9]而不是[1.0, 1.5, 2.0, 2.5]。另外确认pywt.waverec2的输入系数形状和分解时一致,高频子带是元组(cH, cV, cD),不要拆开传。

4.4 现象:CLAHE 增强后颜色失真,人脸变青

九成是因为直接在 RGB 通道上做了 CLAHE。RGB 三通道独立均衡会破坏通道间的相关性,导致颜色偏移。正确做法是转 LAB 或 HSV 空间,只对亮度通道处理。如果已经转 LAB 还是偏色,检查cv2.cvtColor的转换码是不是COLOR_BGR2LAB,OpenCV 读进来是 BGR 不是 RGB,这个顺序搞反了颜色会完全错乱。

4.5 现象:分水岭分割把整张图都标成边界

markers初始化有问题。connectedComponents返回的标记图里背景是 0,前景从 1 开始。如果直接把这个标记图传给watershed,背景 0 会被当作未知区域,分水岭会把所有背景都标成边界。必须做markers = markers + 1把背景变成 1,再把unknown区域设为 0。另外确认sure_fg不是全黑,如果距离变换阈值系数设得太高,sure_fg可能一个前景点都没有。

5. 把分割和增强串成 pipeline:一个可复用的图像处理验证技巧

单独跑通分割和增强只是第一步,实际项目里两者往往要串起来。我一般会搭一个最小验证 pipeline:输入一张原始图,先做 MSR 增强,再送进 U-Net 分割,最后把分割掩码叠加回增强后的图做可视化。这样能快速判断增强是否真的帮助了分割,而不是凭感觉调参。

import cv2 import numpy as np import torch def enhance_and_segment(img_path, model, device='cuda'): # 读取并增强 img = cv2.imread(img_path) img_float = img.astype(np.float32) + 1.0 b, g, r = cv2.split(img_float) enhanced = cv2.merge([ multi_scale_retinex(b), multi_scale_retinex(g), multi_scale_retinex(r) ]) # 准备分割输入:resize 到 256x256,归一化 input_img = cv2.resize(enhanced, (256, 256)) input_tensor = torch.from_numpy(input_img).permute(2, 0, 1).float() / 255.0 input_tensor = input_tensor.unsqueeze(0).to(device) # 推理 model.eval() with torch.no_grad(): pred = model(input_tensor) mask = (pred.squeeze().cpu().numpy() > 0.5).astype(np.uint8) * 255 mask = cv2.resize(mask, (img.shape[1], img.shape[0])) # 叠加可视化:分割区域用红色半透明覆盖 overlay = enhanced.copy() overlay[mask == 255] = [0, 0, 255] result = cv2.addWeighted(enhanced, 0.7, overlay, 0.3, 0) return enhanced, mask, result

这个 pipeline 的验证逻辑是:如果增强后的分割 IoU 比直接在原图上分割高,说明增强有效;如果反而下降,说明增强引入了伪影干扰了模型。我习惯把增强前后的分割结果并排保存,肉眼对比边界贴合度。参数上,MSR 的 sigma 列表和 U-Net 的输入尺寸要匹配,增强后的图像如果噪声明显,先在 MSR 后面加一个cv2.bilateralFilter保边滤波,d=9, sigmaColor=75, sigmaSpace=75是常用起点。

一个容易忽略的细节是分割掩码 resize 回原图尺寸时,用cv2.INTER_NEAREST而不是默认的双线性插值,否则边界会糊掉。另外如果原图分辨率超过 2000×2000,建议先降采样再增强和分割,最后把掩码放大回去,这样速度快很多,精度损失在可接受范围内。

这套源码包最值得花时间的地方,不是每个算法都跑一遍,而是挑两三个和你实际场景最接近的,把参数调透,把失败案例记录下来。我自己的习惯是每调一组参数就存一张对比图,文件名带上参数值,比如clahe_clip2.0_tile8.jpg,回头翻的时候一目了然。图像处理这行,玄学参数不少,但记录做得够细,玄学也能变成经验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询