尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于TensorFlow的老照片修复系统:U-Net去划痕与残差超分实战

发布时间:2026/9/26 10:53:41

资讯中心
01
ARTICLE

基于TensorFlow的老照片修复系统:U-Net去划痕与残差超分实战

基于TensorFlow的老照片修复系统:U-Net去划痕与残差超分实战
简介这是一套面向计算机相关专业学生与开发者的深度学习老照片修复项目源码基于Python与TensorFlow实现并配有Web交互界面与训练好的模型可用于毕业设计、课程设计或项目立项演示。压缩包共21个文件约2.15MB包含7个py脚本负责模型构建与推理、3个html页面搭建上传与预测界面、5个png与2个jpg示例图片、1份docx设计文档及说明md另含2个pyc编译文件目录中可见colorizers、models、utils、templates等模块结构清晰便于二次开发。项目已通过测试功能完整可正常运行适合借鉴学习或在此基础上修改扩展。目前已有163人学习下载。对于想了解图像修复流程、模型部署与Web端集成的读者可从中获取完整的代码实现、界面模板与设计文档快速上手并完成自己的课题任务。1. 老照片修复系统从划痕到高清一套能跑通的深度学习方案家里翻出一张二十年前的全家福扫描进电脑一看人脸模糊、满屏噪点、还有几道折痕划痕。想修PS 门槛太高找人修一张几十上百。这个标题指向的就是用 Python TensorFlow 搭一套深度学习老照片修复系统再配一个 Web 界面让不懂技术的人也能上传照片、点一下、拿到修复结果。它解决的核心问题是把「划痕去除 分辨率提升 噪点抑制」这三件事用模型自动化。适合谁有 Python 基础、想做一个完整可演示项目的学生和初级工程师也适合想理解「深度学习模型怎么包成 Web 服务」的开发者。整套系统拆开看就是四块数据、模型、训练、Web 界面。下面按能复现的顺序讲透。2. 老照片修复到底修什么三类退化与模型选型2.1 先搞清楚老照片的「病」有几种很多人一上来就想用一个模型解决所有问题结果训练完发现划痕没了但脸也糊了。老照片的退化不是单一类型常见的是三种叠加第一种是划痕与折痕表现为图像上随机分布的亮线或暗线这是物理损伤属于「局部结构性缺失」。第二种是噪点与颗粒老胶片扫描后普遍带颗粒噪声暗部尤其明显属于「全局随机噪声」。第三种是分辨率不足与模糊早期相机像素低加上对焦和抖动人脸五官边界不清属于「高频信息丢失」。这三类的修复目标完全不同去划痕是「填补」去噪是「平滑但要保边」超分是「生成原本不存在的高频细节」。所以工程上常见做法是分阶段处理而不是一个模型端到端硬扛。我一般会先做去划痕再做超分最后做一次轻度去噪和锐化。顺序反了会翻车——先超分会把划痕也放大后面更难去。2.2 为什么选 TensorFlow 而不是别的热搜里 tensorflow 和 pytorch 的对比一直没停过。就这个项目而言选 TensorFlow 有两个现实理由一是tf.data做图像管道的吞吐很稳读几万张图不会成为瓶颈二是tf.keras的Model.fit配合ModelCheckpoint、EarlyStopping这些回调训练脚本能写得很短适合把精力放在模型结构上。如果你团队已经在用 PyTorch换过去也没问题模型结构是通的只是数据管道和保存格式要改。模型结构上去划痕我推荐U-Net 类编解码结构因为它的跳跃连接能把浅层纹理直接传到解码端划痕填补后周围纹理不会断。超分推荐残差网络 亚像素卷积残差块负责学高频残差亚像素卷积负责上采样比直接反卷积少很多棋盘格伪影。这两个结构都不复杂下面直接给能跑的代码。提示不要一上来就上 GAN。GAN 训练不稳定判别器一强生成器就出鬼影判别器一弱输出又糊。先用 L1/L2 损失把结构跑通有精力再加感知损失。3. 数据管道老照片数据集怎么造、怎么读3.1 没有配对数据就自己造公开的老照片配对数据集很少而且质量参差。最可靠的做法是用清晰照片反向合成退化拿一批高清人脸或风景图程序化地加划痕、加噪声、降分辨率得到「退化图 → 清晰图」的配对。这样数据量可控退化类型也能按需调。import numpy as np import cv2 import random def add_scratch(img): 在图像上随机画几条划痕模拟折痕损伤 h, w img.shape[:2] out img.copy() for _ in range(random.randint(1, 4)): x1, y1 random.randint(0, w), random.randint(0, h) x2, y2 random.randint(0, w), random.randint(0, h) # 划痕颜色随机偏亮或偏暗宽度 1~3 像素 color 255 if random.random() 0.5 else 0 thickness random.randint(1, 3) cv2.line(out, (x1, y1), (x2, y2), (color, color, color), thickness) return out def degrade(img, scale4): 完整退化流程划痕 - 高斯噪声 - 下采样 img add_scratch(img) noise np.random.normal(0, 12, img.shape).astype(np.float32) img np.clip(img.astype(np.float32) noise, 0, 255).astype(np.uint8) h, w img.shape[:2] # 先高斯模糊再下采样模拟低分辨率镜头的模糊 img cv2.GaussianBlur(img, (5, 5), 1.2) small cv2.resize(img, (w // scale, h // scale), interpolationcv2.INTER_AREA) return small, img # 返回退化图和作为标签的清晰图这段代码的逻辑是add_scratch负责局部结构损伤degrade负责全局噪声和分辨率损失。参数上噪声标准差 12 是个经验值太小模型学不到去噪太大图像信息被淹没scale4表示 4 倍超分显存吃紧就降到 2。注意标签图用的是「加完划痕和噪声但没下采样」的图这样模型同时学去划痕、去噪和超分一步到位。3.2 用 tf.data 搭高效输入管道数据造好后如果每张图都在 Python 里读、解码、增强GPU 会大量空等。正确做法是用tf.data.Dataset把预处理放进图里并行执行。import tensorflow as tf def load_pair(degraded_path, clean_path, size(256, 256)): deg tf.io.read_file(degraded_path) deg tf.image.decode_png(deg, channels3) deg tf.image.resize(deg, size) deg tf.cast(deg, tf.float32) / 127.5 - 1.0 # 归一化到 [-1, 1] cln tf.io.read_file(clean_path) cln tf.image.decode_png(cln, channels3) cln tf.image.resize(cln, size) cln tf.cast(cln, tf.float32) / 127.5 - 1.0 return deg, cln def build_dataset(deg_paths, cln_paths, batch8, shuffleTrue): ds tf.data.Dataset.from_tensor_slices((deg_paths, cln_paths)) if shuffle: ds ds.shuffle(1000) ds ds.map(load_pair, num_parallel_callstf.data.AUTOTUNE) ds ds.batch(batch).prefetch(tf.data.AUTOTUNE) return ds关键参数说明归一化到[-1, 1]而不是[0, 1]是因为后面用tanh激活输出范围要匹配num_parallel_callsAUTOTUNE让 TensorFlow 自己决定并行度不用手调prefetch保证 GPU 算当前 batch 时 CPU 已经在准备下一个。batch 设 8 是 8GB 显存下的稳妥值显存大可以加到 16。注意tf.image.resize默认双线性插值做超分标签时不要用它来生成低分辨率图会引入插值伪影。低分辨率图要用 OpenCV 的INTER_AREA下采样标签图才用 resize 对齐尺寸。4. 模型搭建与训练U-Net 去划痕 残差超分4.1 U-Net 去划痕网络from tensorflow.keras import layers, Model def conv_block(x, filters): x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) return x def build_unet(input_shape(256, 256, 3)): inputs layers.Input(shapeinput_shape) # 编码器逐层下采样提取语义 c1 conv_block(inputs, 32) p1 layers.MaxPooling2D()(c1) c2 conv_block(p1, 64) p2 layers.MaxPooling2D()(c2) c3 conv_block(p2, 128) # 瓶颈层 b conv_block(c3, 256) # 解码器上采样并与编码器特征拼接 u1 layers.UpSampling2D()(b) u1 layers.Concatenate()([u1, c3]) d1 conv_block(u1, 128) u2 layers.UpSampling2D()(d1) u2 layers.Concatenate()([u2, c2]) d2 conv_block(u2, 64) u3 layers.UpSampling2D()(d2) u3 layers.Concatenate()([u3, c1]) d3 conv_block(u3, 32) # 输出层用 tanh匹配 [-1,1] 的标签范围 outputs layers.Conv2D(3, 1, activationtanh)(d3) return Model(inputs, outputs)结构逻辑编码器每下采样一次感受野翻倍能「看到」更大范围的划痕上下文解码器的Concatenate把对应层的浅层特征接回来保证纹理细节不丢。输出用tanh是因为标签归一化到了[-1,1]用sigmoid会导致输出偏灰。损失函数用 L1 而不是 L2L1 对划痕这种稀疏损伤更鲁棒L2 容易把划痕区域平滑成一片模糊。4.2 残差超分网络与训练循环def residual_block(x, filters64): shortcut x x layers.Conv2D(filters, 3, paddingsame, activationrelu)(x) x layers.Conv2D(filters, 3, paddingsame)(x) return layers.Add()([shortcut, x]) def build_sr(scale4): inputs layers.Input(shape(None, None, 3)) x layers.Conv2D(64, 9, paddingsame, activationrelu)(inputs) for _ in range(8): # 8 个残差块够用且不爆显存 x residual_block(x) x layers.Conv2D(64, 3, paddingsame)(x) # 亚像素卷积上采样scale4 需要两次 x2 x layers.Conv2D(3 * scale ** 2, 3, paddingsame)(x) outputs layers.Lambda( lambda t: tf.nn.depth_to_space(t, scale) )(x) return Model(inputs, outputs) unet build_unet() unet.compile(optimizertf.keras.optimizers.Adam(1e-4), lossmae) unet.fit(train_ds, validation_dataval_ds, epochs50, callbacks[ tf.keras.callbacks.ModelCheckpoint(unet_best.h5, save_best_onlyTrue), tf.keras.callbacks.EarlyStopping(patience8, restore_best_weightsTrue) ])depth_to_space是亚像素卷积的核心把通道维的scale²倍数据重排成空间维比反卷积少伪影。学习率 1e-4 是 Adam 的常用起点训练几轮后 loss 不降就降到 1e-5。EarlyStopping的patience8表示验证 loss 连续 8 轮不改善就停避免过拟合。训练时盯着验证集如果训练 loss 一直降但验证 loss 反弹就是过拟合了加 dropout 或减残差块数量。提示两个模型分开训练先训 U-Net 去划痕冻结它再用它的输出训超分。联合训练容易互相干扰调参成本翻倍。5. 避坑与排查训练和部署里最容易翻车的五件事5.1 输出全灰或全黑现象模型训练完推理出来是一张灰色或纯黑的图。原因最常见是归一化范围不匹配——训练标签是[-1,1]推理时输入却按[0,255]喂进去或者输出层激活函数和标签范围对不上。解决统一检查三处数据归一化、输出层激活、推理前处理。写一个assert检查输入范围能省几小时排查。5.2 划痕去掉了但人脸变塑料现象划痕确实没了但皮肤像磨皮过度五官细节丢失。原因L2 损失或过大的去噪强度把高频纹理一起抹平了。解决损失换成 L1 或 Charbonnier 损失在损失里加一项感知损失用预训练 VGG 提特征算距离逼模型保留语义细节。感知损失的权重从 0.01 开始试太大输出会出伪影。5.3 显存溢出OOM现象训练到一半报ResourceExhaustedError。原因batch 太大、输入尺寸太大或者tf.data的prefetch缓存了太多 batch。解决先把 batch 降到 4 或 2输入从 256 降到 128prefetch参数从AUTOTUNE改成固定 2。如果还不行用混合精度tf.keras.mixed_precision.set_global_policy(mixed_float16)显存能省近一半。5.4 Web 上传后推理超时现象Web 界面点上传转圈很久最后超时。原因推理在请求线程里同步跑大图没缩放直接进模型或者模型每次请求都重新加载。解决模型在服务启动时加载一次全局复用上传图先缩到模型输入尺寸再推理推理放到线程池或队列里别阻塞 Web 主线程。Flask 下用ThreadPoolExecutor就能解决。5.5 训练 loss 正常但验证集效果差现象训练 loss 降到很低验证集 PSNR 却上不去。原因训练集和验证集的退化分布不一致比如训练集划痕多、验证集噪点多或者验证集图片和训练集有重叠数据泄漏。解决合成退化时保证训练和验证用同一套退化参数分布划分数据集时按「原图」划分同一张原图合成的所有退化图只能进同一个集合不能跨集。6. 把模型包成 Web 服务Flask 接口与前端联调6.1 最小可用的推理接口from flask import Flask, request, send_file import numpy as np import cv2 import io from tensorflow.keras.models import load_model app Flask(__name__) # 服务启动时加载一次避免每次请求重复加载 unet load_model(unet_best.h5, compileFalse) sr load_model(sr_best.h5, compileFalse) app.route(/restore, methods[POST]) def restore(): file request.files[image] data np.frombuffer(file.read(), np.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img.shape[:2] # 缩放到模型输入尺寸记录缩放比用于还原 inp cv2.resize(img, (256, 256)) inp inp.astype(np.float32) / 127.5 - 1.0 inp np.expand_dims(inp, 0) # 先去划痕再超分 cleaned unet.predict(inp)[0] cleaned ((cleaned 1) * 127.5).astype(np.uint8) sr_in np.expand_dims(cleaned.astype(np.float32) / 127.5 - 1.0, 0) upscaled sr.predict(sr_in)[0] upscaled ((upscaled 1) * 127.5).clip(0, 255).astype(np.uint8) # 编码回 PNG 返回 _, buf cv2.imencode(.png, cv2.cvtColor(upscaled, cv2.COLOR_RGB2BGR)) return send_file(io.BytesIO(buf.tobytes()), mimetypeimage/png) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)逻辑说明load_model放在模块顶层Flask 启动时执行一次compileFalse是因为推理不需要优化器和损失能加快加载。推理流程严格按「去划痕 → 超分」顺序。threadedTrue让 Flask 能并发处理请求但真正的瓶颈在模型推理生产环境建议上 gunicorn 多 worker。6.2 前端联调要注意的三件事前端就是一个上传框加一个预览区用fetch发FormData到/restore。联调时最容易出问题的是跨域和图片格式。跨域用flask-cors一行解决图片格式上前端上传的可能是 JPEG但模型按 RGB 处理后端imdecode后要确认通道顺序BGR 转 RGB 这步漏了输出颜色会偏蓝。另外前端要限制上传文件大小比如 10MB否则一张几十 MB 的扫描图传上来后端解码就吃满内存。预览区用URL.createObjectURL显示原图修复结果用返回的 blob 显示两边并排对比用户一眼能看到效果。注意Web 界面只是演示层别把训练逻辑塞进去。训练和推理分离模型文件通过挂载或对象存储共享服务才能独立扩缩容。7. 进阶技巧用滑动窗口处理任意尺寸大图模型训练时输入固定 256×256但用户上传的照片可能是 4000×3000。直接缩到 256 再放大细节全丢了。正确做法是滑动窗口分块推理再拼接这也是热搜里「滑动窗口滤波模型」在图像修复里的典型用法。def sliding_window_inference(model, img, patch256, stride192): 分块推理stride patch 保证块间有重叠消除拼接缝 h, w img.shape[:2] output np.zeros((h, w, 3), np.float32) weight np.zeros((h, w, 1), np.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y patch, h) x2 min(x patch, w) y1 max(0, y2 - patch) x1 max(0, x2 - patch) patch_img img[y1:y2, x1:x2] inp patch_img.astype(np.float32) / 127.5 - 1.0 pred model.predict(np.expand_dims(inp, 0))[0] pred (pred 1) * 127.5 output[y1:y2, x1:x2] pred weight[y1:y2, x1:x2] 1 return (output / np.maximum(weight, 1)).astype(np.uint8)关键参数是stride它必须小于patch重叠区域用来做加权平均消除块与块之间的接缝。stride192表示重叠 64 像素接缝基本看不出来。代价是计算量增加约 1.8 倍但换来的是全分辨率修复值得。如果图片特别大可以先把长边缩到 2000 以内再滑窗速度和质量的平衡点。验证修复效果别只看眼睛算一下 PSNR 和 SSIM。PSNR 低于 25dB 说明结构偏差大SSIM 低于 0.85 说明纹理丢失明显。我一般会在验证集上跑一遍把指标最差的几张挑出来看往往能发现某类退化没覆盖到回头补数据。这套系统我从数据合成到 Web 上线跑通过不止一次最大的教训是别在模型结构上炫技数据质量和退化分布匹配才是决定效果的上限。先把 U-Net 和残差超分跑通指标稳住再考虑加注意力、加 GAN。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。