ARTICLE DETAIL

资讯详情

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

OpenCV与深度学习图像背景去除:Tiramisu模型与Python实战

OpenCV与深度学习图像背景去除:Tiramisu模型与Python实战 简介基于OpenCV与深度学习的图像背景去除项目主要面向具备基础Python知识的图像处理开发者适用于人像抠图、物体分割、图像编辑预处理等场景。项目集成预训练模型配置与模型结构示意图提供人像和非人像两套处理脚本并附带太阳镜移除的扩展示例可在Windows10与Python3.6.5环境下直接运行复现。资源包共47个文件由3个Python脚本、JSON模型配置、依赖清单、说明文档及数十张输入输出示例图片组成压缩后约35.83MB目录按images分组存放便于对照结果开展学习。目前已有880人学习过该资源整体设计紧凑适合作为图像分割入门参考。借助成对出现的输入与原输出图可直观理解深度模型在不同对象上的去背景效果结合脚本注释与结构文档也能方便地调整参数或迁移到自己的图像处理任务中。1. 用 OpenCV 和深度学习去除图像背景这套 Python 代码值得一试做图像处理的人迟早都会撞上「抠图」这道坎。传统方法用颜色阈值、边缘检测、分水岭碰到复杂背景就崩用 Photoshop 钢笔工具慢慢勾一张图耗半小时。这套基于 OpenCV 和深度学习的背景去除方案走的是语义分割路线——用一个 Tiramisu 模型对图像逐像素分类判断每个像素属于前景还是背景然后直接把背景抹掉。模型在 model.json 里定义架构person.py 和 non-person.py 两个脚本分别处理「有人物」和「无人物」两类图你不需要训练模型只要把环境和权重配好就能直接跑。适合刚入门深度学习分割、需要在本地批量处理图片、又不想折腾复杂标注流程的人。说句直接的这不是那种只讲原理的 Demo 教学而是一个能立刻出图的完整工程。下面我把模型结构、运行流程、参数含义和容易翻车的点全拆开讲。2. 深扒这套方案的核心为什么选 Tiramisu以及 person 与 non-person 的分工逻辑2.1 背景去除的本质是逐像素分类不是抠轮廓传统抠图是先找边缘再把边缘围出来的区域当作前景。这种做法对边缘清晰的物体有效但对头发丝、半透明物体、复杂纹理完全没有抵抗力。深度学习方法换了个思路把整张图喂给神经网络网络输出一张和原图一样大的概率图每个像素的值表示「它属于前景」的置信度。最后拿一个阈值把这个概率图二值化得到 mask再用 mask 把原图的背景像素置零。这套方案用的是 TiramisuOne Hundred Layers Tiramisu一个基于 DenseNet 改造的语义分割模型。它与 U-Net 一样有编码器-解码器结构区别在于每一层都用了 Dense Block——每一层的输入是前面所有层输出的拼接。这样做的好处是梯度传导路径短训练时不容易梯度消失而且参数利用率高对小数据集特别友好。对比项Tiramisu本项目U-NetDeepLabV3基础结构DenseNet 跳跃连接卷积 跳跃连接空洞卷积 ASPP参数量中等较大更大对小数据集适应性强中等弱推理速度快中等较慢从使用角度看这套方案做背景去除的效果在中等复杂度图片上表现不错——背景比较干净、主体明确的情况下输出的 mask 边缘比较准确。但如果背景和前景颜色接近或者主体边缘有大量毛发还是会出现边缘毛刺需要后处理修正。明白这一点你就不会对输出结果有不切实际的期待。2.2 model.json 里装的是什么模型架构与权重分离的设计打开压缩包你会发现 model.json 和图片文件放在同一层人像和非人像两个脚本都在根目录。model.json 是 Keras 的模型架构文件用 JSON 格式保存了网络的每一层配置——层类型、卷积核大小、滤波器数量、激活函数、跳跃连接关系等。这种设计与权重文件分离的好处是显而易见的你改模型结构只动 json不用碰权重换权重也不用反序列化整个模型。加载方式用 Keras 标准做法from keras.models import model_from_json with open(model.json, r) as f: model model_from_json(f.read()) model.load_weights(weights.h5)这段代码做了两件事先读取 model.json 重建模型结构再把训练好的权重加载进来。注意model_from_json只恢复网络结构不恢复权重。如果你遇到自定义层报错说明模型结构里有 Keras 不自带的层需要在加载前把自定义层对象传入。很多人在这一步栽跟头后面第 4 章我会展开讲。那为什么 Tiramisu 的推理代码是「逐张处理」而不是「批量池化加载」因为背景去除的场景通常是处理单张图片或小批量图片逐张处理可以灵活控制内存占用。而且这个模型原始的输入尺寸是 224×224 或 256×256逐张处理可以把原图先缩放再推理最后再把 mask 缩放回原图尺寸。这样做比直接拉伸到任意尺寸再推理更稳——网络没见过训练时没见过的分辨率直接上大图会产生奇怪的伪影。2.3 person.py 与 non-person.py 的差别不只是换个输入路径两个脚本的功能边界很清晰person.py 处理有人物的照片比如人像合影、单人照non-person.py 处理没有人的场景比如宠物、汽车、家具、风景中的孤立主体。它们共享同一个模型架构和权重区别主要在后处理逻辑上。person.py 会更激进地保留「人体区域」mask 会更偏向中心区域对人像边缘做额外的腐蚀或膨胀处理避免背景残留在人物轮廓上。non-person.py 对 mask 的处理更保守严格按模型输出的概率值走不做额外的形态学调整因为物体形状比人像更规则。这种设计思路说白了就是用调参来弥补模型精度的不足——同一个模型针对不同场景调整后处理参数比重新训练一个模型便宜得多。我看到很多初学者拿 person.py 去处理「没有人的图」出来的 mask 经常把大块背景误判为前景反过来用 non-person.py 处理人像又容易出现人像边缘缺损。你先判断图片类型再选择脚本这是整套路线的第一步。3. 把代码跑起来Python 3.6.5 环境配置、模型加载与两类脚本运行3.1 环境搭建版本怎么锁依赖怎么装项目摘要里明确写了运行环境Python 3.6.5、Windows 10。这套组合在 2024 年的今天看起来偏老但别急着升级——很多 TensorFlow 1.x 时代的模型在 Python 3.8 以上会遇到兼容性问题。如果你手头没有 Python 3.6.5我建议直接用 Anaconda 建一个独立环境不要影响你平时的开发环境。安装顺序和命令conda create -n bgremove python3.6.5 conda activate bgremove pip install tensorflow1.14.0 pip install keras2.2.5 pip install opencv-python3.4.2.17 pip install numpy1.17.4 pip install pillow为什么锁这些版本TensorFlow 1.14 是 1.x 系列里对 Windows 支持比较稳的版本和 Keras 2.2.5 搭配不会出现莫名其妙的自定义层报错。opencv-python 3.4 系列兼容 Python 3.6cv2.imread的中文路径问题也相对可控。numpy 锁 1.17.4 是因为 TensorFlow 1.14 与 numpy 1.18 以上的版本存在 API 冲突容易在导入时报_ARRAY_API not found的错误。装完建议立刻验证一遍导入python -c import cv2; import keras; import tensorflow; print(ok)看到ok再往下走。这一步能挡住后面 80% 的环境类报错。OpenCV 安装教程里经常让你直接pip install opencv-python但在这个老项目上装最新版 OpenCV 大概率会踩到cv2.findContours返回值数量不一致的坑——旧代码按 2 个返回值写新 OpenCV 返回 3 个值。3.2 模型加载与单张图片推理流程模型加载的核心逻辑在前文代码里这里补完整推理流程。person.py 和 non-person.py 的整体结构类似区别体现在后处理函数的参数上。完整流程如下import cv2 import numpy as np from keras.models import model_from_json # 加载模型 with open(model.json, r) as f: model model_from_json(f.read()) model.load_weights(weights.h5) # 读取图片并缩放到模型输入尺寸 img cv2.imread(input.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img.shape[:2] # 保存原图尺寸推理后要还原 # 模型要求输入是浮点型且归一化到 [0, 1] input_tensor cv2.resize(img, (224, 224)) input_tensor input_tensor.astype(np.float32) / 255.0 input_tensor np.expand_dims(input_tensor, axis0) # 加 batch 维度 # 推理输出是 (1, 224, 224, num_classes) 的概率图 output model.predict(input_tensor)[0] # 把概率图缩回原图尺寸 mask cv2.resize(output, (w, h))几个参数需要说明。224是模型的输入尺寸这个值不能随便改——模型第一层的输入形状写死在 model.json 里你传别的分辨率会在 predict 时报 shape mismatch。/255.0是归一化操作Tiramisu 训练时图像值域被归一化到 0-1你直接喂原始像素值输出的概率图会失去意义。np.expand_dims(input_tensor, axis0)是把单张图变成 batch 为 1 的四维张量Keras 的 predict 接口要求输入必须是四维的。如果你想看模型对这张图的「判断过程」可以输出中间层的特征图。不过对工程应用来说直接拿最终概率图就够了。真正要调的是后处理这一步。3.3 把概率图变成干净 mask阈值、形态学与抠图输出模型输出的概率图不能直接用来剪贴背景你需要做三件事二值化、形态学清理、按 mask 提取前景。这一段是整套流程里最出效果的部分也是最容易翻车的部分。# 二值化概率大于 0.5 的像素认为是前景 _, binary_mask cv2.threshold(mask, 0.5, 255, cv2.THRESH_BINARY) binary_mask binary_mask.astype(np.uint8) # 形态学操作去掉噪点填补空洞 kernel np.ones((5, 5), np.uint8) binary_mask cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel) binary_mask cv2.morphologyEx(binary_mask, cv2.MORPH_OPEN, kernel) # 边缘平滑 blurred_mask cv2.GaussianBlur(binary_mask, (5, 5), 0) # 用 mask 去除背景前景区域保留原像素背景置 0 foreground cv2.bitwise_and(img, img, maskblurred_mask) cv2.imwrite(output.png, foreground)cv2.threshold的0.5是前景/背景的分界概率。这个值不是固定的——背景越干净阈值可以调高到 0.6 甚至 0.7减少误判背景越杂阈值要适当降到 0.4避免把真前景丢掉。MORPH_CLOSE先膨胀后腐蚀用来填补 mask 内部的小孔洞MORPH_OPEN先腐蚀后膨胀用来去掉背景区域零散的白色噪点。(5, 5)的 kernel 尺寸适用于大多数图片但你处理高分辨率大图时可以按比例调大。cv2.bitwise_and这一步很多人看不太懂img和img自己按位与听上去是重复操作。其实不是——关键是mask参数。mask 为 255 的像素bitwise_and保留img的原像素值mask 为 0 的像素输出直接置 0。所以你是在用 mask 决定「保留哪些像素」不是真的在做两张图的按位与。如果只想要透明背景 PNG把最后两步换成bgra cv2.cvtColor(img, cv2.COLOR_BGR2BGRA) bgra[:, :, 3] blurred_mask # 把 mask 写入 alpha 通道 cv2.imwrite(output_transparent.png, bgra)这样输出的图片在 Photoshop 或网页里都能直接显示透明效果。注意cv2.imwrite保存 PNG 时才支持 alpha 通道存 JPG 会把透明部分压成黑色。3.4 批量图片处理目录遍历与结果输出单张跑通了批量处理就是顺手的事。处理大量图片时我习惯先建输出目录避免把原图和结果混在一起。脚本逻辑很简单遍历输入目录里的所有图片逐张处理按原文件名写入输出目录。import os import glob input_dir images output_dir output os.makedirs(output_dir, exist_okTrue) for img_path in glob.glob(os.path.join(input_dir, *.jpg)) \ glob.glob(os.path.join(input_dir, *.png)): img cv2.imread(img_path) if img is None: print(f[跳过] 无法读取: {img_path}) continue # 推理 后处理代码同单张流程省略 # ... basename os.path.basename(img_path) out_path os.path.join(output_dir, basename) cv2.imwrite(out_path, foreground) print(f[完成] {basename})这个脚本有几个细节值得注意。cv2.imread读取失败时返回None直接传进后面的处理流程会在第 4 行就崩了——所以if img is None的检查必须保留。glob拼接时同时匹配.jpg和.png但cv2.imread不支持 WebP 这类格式如果图片目录里混入了特殊格式建议用os.listdir加后缀白名单的方式更可控valid_ext {.jpg, .jpeg, .png, .bmp} for fname in os.listdir(input_dir): ext os.path.splitext(fname)[1].lower() if ext not in valid_ext: continue img_path os.path.join(input_dir, fname)批量处理还有一个性能问题每次循环都重新加载权重文件会很慢。正确做法是把模型加载放在循环外面只在程序启动时加载一次。如果你有多张图要处理这个改动能把总耗时缩短好几倍。4. 避坑与排查背景去除这个项目最常见的五个翻车点4.1 现象导入cv2报ModuleNotFoundError: No module named cv2很多人在安装 OpenCV 后用import cv2直接报错。原因通常是 pip 装到了错误的 Python 环境里——Windows 上多个 Python 共存时conda环境和系统 Python 各管各的包你在终端激活了某个虚拟环境但pip install opencv-python装到了另一个环境。解决方法是先确认当前环境的 Python 路径再安装which python python -m pip install opencv-python3.4.2.17用python -m pip而不是pip可以保证包装进当前 python 解释器对应的环境里。装完后直接在同一个终端里验证python -c import cv2; print(cv2.__version__)如果你在 PyCharm 里跑代码报错而命令行里不报错多半是 PyCharm 的 Project Interpreter 指向了另一个环境。去Settings Project Python Interpreter里改成你安装了依赖的环境就行。4.2 现象model_from_json加载报错提示 Unknown layer 或无法识别类项目里的 model.json 定义了 Tiramisu 网络结构其中一些层是自定义层比如某些特殊激活函数或归一化层。Keras 2.2.5 内置的层类型能覆盖大部分定义但如果 json 里有它不认识的类名反序列化就会报Unknown layer: xxx。原因一般有两个一是 Keras 版本不对二是这个项目用了当时自定义的层但代码没把自定义层注册进去。解决方式是在加载架构之前手动注册自定义层from keras.utils import CustomObjectScope with CustomObjectScope({某自定义层类名: 某自定义层类}): with open(model.json, r) as f: model model_from_json(f.read())如果你不知道自定义层的类名是什么在报错信息里会写得很清楚。最常见的处理是把项目代码里包含层定义的模块 import 进来因为层类定义在模块里Keras 在反序列化时会根据 json 里的类名在当前命名空间里找。你也检查一下requirements.txt里的 Keras 版本如果锁的是 2.2.x就按我前面给的版本装不要用 Keras 2.4 或更高版。4.3 现象推理完成后 mask 是全黑的输出图片变成纯黑整张 mask 全黑说明模型预测所有像素的概率都低于阈值。最典型的原因是输入图片没有归一化。模型训练时输入是 [0, 1] 范围的浮点数你直接用cv2.imread读进来的 uint8 数组范围 0-255喂给模型所有像素值都偏大经过卷积和激活函数后输出概率几乎全部压到 0。加上astype(np.float32) / 255.0再重新推理一次。cv2.imread的默认颜色通道是 BGR而模型训练时用的是 RGB。如果你忘了cv2.cvtColor(img, cv2.COLOR_BGR2RGB)模型拿到的通道顺序是错乱的——尤其对人物皮肤、绿色植物这类对颜色敏感的内容输出的 mask 会变得很奇怪。全黑不是通道顺序导致的典型表现但会造成 mask 区域边缘错位值得一提。4.4 现象mask 边缘有白边或黑边抠出来的图像贴了一块补丁mask 把前景和背景的边界处理得太硬二值化阈值一刀切边缘像素被强制分成前景或背景抠出来的图边缘会有明显的锯齿感或者背景残留。典型表现是深色背景下人物发丝边缘挂了一圈浅色噪点浅色背景下边缘出现深色描边。解决办法是引入羽化边缘的流程把二值 mask 换成带模糊过渡的 mask# 先做较宽的高斯模糊再叠加到原 mask 上 feather_mask cv2.GaussianBlur(binary_mask.astype(np.float32), (15, 15), 0) feather_mask np.clip(feather_mask, 0, 255).astype(np.uint8)这一步让 mask 在边界处不是从 255 直接掉到 0而是经过 30-50 个像素的渐变过渡。前景和背景的混合区域不再生硬视觉上自然很多。需要说明的是这只是一种轻量级的边缘软化手段——如果你要处理的是发丝级别的精细抠图这套方法做不到你得去用基于 matting 的算法。去白边的另一个实用技巧在cv2.bitwise_and之前对二值 mask 做一次轻微的腐蚀操作让 mask 整体向内收缩 1-2 个像素。原因是前景物体边缘的像素值通常是前景色和背景色混合的结果完全保留会让最终输出图边缘带一圈浅色光晕。4.5 现象numpy 或 TensorFlow 报错提示_ARRAY_API not found或版本冲突这个报错近几年出现的频率特别高原因是 numpy 升级到了 1.19 之后TensorFlow 1.14 引用的旧 API 被移除了。你按照pip install numpy1.17.4重新锁一下版本然后在安装 TensorFlow 之后再装 numpy——顺序有讲究后装的包会覆盖先装包的依赖版本如果你先装 numpy 后装 TensorFlowTensorFlow 会把 numpy 降到它需要的版本有时反而降过头。还有一个冷门的坑Windows 上 TensorFlow 1.14 需要Microsoft Visual C Redistributable for Visual Studio 2015-2019。如果环境里缺这个运行库import tensorflow会在加载 DLL 时直接崩溃报DLL load failed。装上运行库再重试很多人卡在这里半天以为是 Python 或者 TensorFlow 装坏了。出现DLL load failed时别先怀疑代码先确认运行库、确认 Python 位数是 64 位的——TensorFlow 1.x 不支持 32 位 Python。5. 进阶技巧用 Alpha 通道保存透明背景顺便把整个人像抠图流程写成可复用函数你前几步能跑通后面真正要面对的问题是多张图要处理每次都要改一堆参数怎么把流程稳定下来我的做法是把整个流程写成一个函数参数全部显式暴露。这样换图只需要改参数不用改逻辑。函数里有一个值得注意的细节处理完的 mask 不要直接二值化先对 mask 做一次中值滤波去孤立的噪点再做阈值。中值滤波比形态学开运算对细小区域的保留效果更好能保住一些稀疏的前景像素。def remove_bg(img_path, output_path, threshold0.5, kernel_size5, feather0): img cv2.imread(img_path) if img is None: print(f读取失败: {img_path}) return False raw img.copy() img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img.shape[:2] input_tensor cv2.resize(img, (224, 224)) input_tensor input_tensor.astype(np.float32) / 255.0 input_tensor np.expand_dims(input_tensor, axis0) output model.predict(input_tensor)[0] mask cv2.resize(output, (w, h)) # 中值滤波去孤立噪点 mask cv2.medianBlur((mask * 255).astype(np.uint8), 5) _, binary cv2.threshold(mask, threshold * 255, 255, cv2.THRESH_BINARY) if feather 0: binary cv2.GaussianBlur(binary, (feather * 2 1, feather * 2 1), 0) if kernel_size 0: kernel np.ones((kernel_size, kernel_size), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 输出透明背景 PNG bgra cv2.cvtColor(raw, cv2.COLOR_BGR2BGRA) bgra[:, :, 3] binary cv2.imwrite(output_path, bgra) return Trueswitch 几个参数看一眼效果threshold0.5是最常用的分界值。背景干净调到 0.6背景杂乱调到 0.4。kernel_size5时做形态学清理kernel 越大 mask 内部越干净但边缘损失越严重。feather3时边缘带羽化过渡适合贴到深色背景或需要柔和边缘的场景。用的时候注意model需要在这个函数外部先加载好函数内部不做模型加载避免每调一次函数就重新加载一次权重。批量调用的方式也简单model load_model_from_json(model.json, weights.h5) for img_path in image_list: remove_bg(img_path, fout_{os.path.basename(img_path)}, threshold0.55, kernel_size5, feather3)输出透明背景 PNG 后放进 PPT 或者网页里都不需要额外去底。如果你要的是白色背景而非透明背景把最后两步换成cv2.bitwise_and再将结果拷贝到一张白色底图上就行灵活处理即可。这个项目我用下来最大的体感是真正的坑基本不在模型本身而在环境和后处理——版本一不对就报错阈值一不对就出毛边。从那以后我每次跑这类老项目都会强制走一遍「锁版本 → 验证导入 → 单图测试 → 批量跑」四个步骤省下的排查时间远大于装环境花掉的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表