图像去雾算法研究系统:暗通道先验与Django/MySQL全栈实现 简介这是一套面向毕业设计的完整图像去雾算法研究系统借助Python与Django框架搭建覆盖前端交互、后端逻辑和MySQL数据库存储适合计算机相关专业学生进行课程设计或毕业设计参考。压缩包共250个文件约48.39MB包含37个Python源码、38个编译后的pyc文件、76个演示动图、35个前端脚本、14个样式表、9个网页文件以及数据库脚本、答辩演示文稿和详尽的说明文档。系统允许用户上传含雾图片并实时观察去雾效果后端可集成暗通道先验、基于Retinex理论等多种常见算法便于开展算法对比与效果调试。资源目录呈现清晰已具备bootstrap、layui等前端框架文件目前已有71人学习使用。借助附带说明文档与PPT可帮助理解系统架构、模块划分、算法数学原理和关键代码实现对图像去雾应用的二次开发、功能扩展或论文撰写具有较高参考价值。1. 图像去雾算法研究系统毕业设计里最容易被问崩的一个点图像去雾算法研究系统说白了就是把一张带雾的图片上传到网页后端跑一遍去雾算法再把清晰结果返回给浏览器同时把处理前后的图片、处理时间、算法参数存进数据库供用户随时回看。很多人在答辩时被问崩不是因为算法跑不出来而是被问到“你的去雾原理是什么”“数据库为什么这么设计”“图片存哪里了”时答不上来。这篇笔记把整条链路拆开讲——暗通道先验算法怎么落地成 Python 代码、Django 怎么接图片上传、MySQL 表怎么建、哪几个坑是毕业设计必踩的。适合正在做毕设、想把“算法 Web 系统”做成一套完整交付物的人。2. 暗通道先验去雾核心算法怎么落地成可复现的代码2.1 为什么毕业设计首选暗通道先验图像去雾算法有很多条路线基于深度学习的 AOD-Net、DehazeNet 效果确实好但普通本科毕设跑深度学习模型要面对数据集标注、GPU 资源、训练时长这些问题周期拉长风险变大。传统方法里暗通道先验Dark Channel Prior是何恺明 2009 年提出的经典工作效果肉眼可见公式推导能讲清楚实现代码不超过 50 行不用 GPU普通笔记本就能跑——这三个特点让它成为毕业设计里性价比最高的选择。暗通道先验的核心观察是在无雾的清晰户外图像里绝大多数局部区域除了天空区域的 RGB 三个通道里至少有一个通道的像素值非常低接近 0。这个“局部区域最低通道值”构成的图就是暗通道。而有雾图像因为大气光散射暗通道值会被抬高。基于这个差异可以从单张有雾图反推透射率和大气光进而恢复清晰图像。2.2 最小可复现的暗通道去雾代码我一般会把算法单独拆成一个dehaze.py文件不混进 Django 的 views 里这样以后换算法、调参、写单元测试都方便。下面是完整可跑的最小实现import cv2 import numpy as np def dark_channel(img, size15): 计算暗通道图 :param img: BGR 图像0-255 :param size: 最小值滤波窗口大小 :return: 暗通道图单通道 b, g, r cv2.split(img) min_channel cv2.min(cv2.min(r, g), b) # 逐像素取三通道最小值 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (size, size)) dark cv2.erode(min_channel, kernel) # 最小值滤波相当于腐蚀 return dark def estimate_atmospheric_light(img, dark, top_percent0.001): 估计全局大气光 A 值 选取暗通道中最亮的前 0.1% 像素取原图中这些像素的最大值作为 A h, w dark.shape num_pixels h * w top_num max(1, int(num_pixels * top_percent)) dark_flat dark.ravel() img_flat img.reshape(-1, 3) indices np.argsort(dark_flat)[-top_num:] # 暗通道最亮的像素下标 atmospheric np.max(img_flat[indices], axis0) # 对应原图中的最大值 return atmospheric.astype(np.float64) def transmission_estimate(img, A, omega0.95, size15): 估计透射率 t(x) normalized img.astype(np.float64) / A dark_normalized dark_channel(normalized, size).astype(np.float64) t 1.0 - omega * dark_normalized return np.clip(t, 0.1, 1.0) # 下限 0.1 防止分母过小导致噪声放大 def guided_filter(guide, src, radius40, eps1e-3): 导向滤波细化透射率图避免边缘 halo 效应 guide guide.astype(np.float64) src src.astype(np.float64) mean_i cv2.boxFilter(guide, ddepth-1, ksize(radius, radius)) mean_p cv2.boxFilter(src, ddepth-1, ksize(radius, radius)) mean_ip cv2.boxFilter(guide * src, ddepth-1, ksize(radius, radius)) cov_ip mean_ip - mean_i * mean_p mean_ii cv2.boxFilter(guide * guide, ddepth-1, ksize(radius, radius)) var_i mean_ii - mean_i * mean_i a cov_ip / (var_i eps) b mean_p - a * mean_i mean_a cv2.boxFilter(a, ddepth-1, ksize(radius, radius)) mean_b cv2.boxFilter(b, ddepth-1, ksize(radius, radius)) return mean_a * guide mean_b def dehaze(img, omega0.95, window_size15): 主流程输入 BGR 雾图返回去雾图 img img.astype(np.float64) dark dark_channel(img, window_size) A estimate_atmospheric_light(img, dark) t transmission_estimate(img, A, omega, window_size) # 以原图灰度图作为导向图细化透射率 gray cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY) t_refined guided_filter(gray, t) # 恢复公式 J (I - A) / t A t_stack np.stack([t_refined] * 3, axis-1) result (img - A) / t_stack A return np.clip(result, 0, 255).astype(np.uint8)这段代码的逻辑链很直白先算暗通道图从暗通道最亮的 0.1% 像素里估计出大气光 A再用 A 归一化图像算出透射率 t导向滤波把 t 细化一遍避免物体边缘出现白色光晕最后套恢复公式得到清晰图。三个参数要记住omega控制去雾强度0.85 到 0.95 之间常用调太高画面偏暗、颜色失真window_size是暗通道的最小值滤波窗口15 是论文默认值窗口越大暗通道越“粗糙”天空区域容易过曝导向滤波的radius40控制边缘保真度图像分辨率超过 2000 像素时建议调到 60。2.3 批量测试工具拿真实雾图验证效果算法写完了单张图看不出好坏我习惯写一个批量脚本把测试目录里的雾图全部跑一遍输出结果图和一个简单的 CSV 评估记录。这样答辩时能直接展示“我测了 50 张图平均处理时间多少秒”——这比口头说“效果不错”有说服力得多。import os import time import csv import cv2 from dehaze import dehaze input_dir test_foggy output_dir test_result os.makedirs(output_dir, exist_okTrue) records [] for name in os.listdir(input_dir): path os.path.join(input_dir, name) img cv2.imread(path) if img is None: continue start time.time() result dehaze(img) elapsed time.time() - start out_path os.path.join(output_dir, name) cv2.imwrite(out_path, result) records.append([name, f{elapsed:.3f}s, img.shape[1], img.shape[0]]) print(f{name} 完成耗时 {elapsed:.3f}s) with open(eval_results.csv, w, newline) as f: writer csv.writer(f) writer.writerow([文件名, 耗时, 宽度, 高度]) writer.writerows(records)跑批量之前先确认输入目录里没有中文文件名OpenCV 的imread遇到中文路径会静默返回None程序不报错但会跳过图片。另外如果测试图里有纯白天空占比很大的图暗通道先验会把天空区域错误增强成灰黑色块这不是代码 bug而是算法本身的局限答辩时被问到就如实说“暗通道先验在天空区域的先验假设不成立可以加分割或亮通道补偿”。3. Django 后端与图片上传链路把一张雾图从浏览器送到算法再返回3.1 创建项目与 App 划分算法、业务和用户分开拿到一套 Django 项目源码第一件事不是急着跑而是先看目录结构。标准做法是一个项目放全局配置两个 App 分别管用户与业务、管算法调用。我自己的习惯是拆成account和dehaze_appaccount管登录注册和用户信息dehaze_app管图片上传、去雾任务记录、历史列表展示。这样后面加“管理员审核图片”或者“给不同用户分配不同算法权限”时不需要动全局配置文件。django-admin startproject dehaze_system cd dehaze_system python manage.py startapp dehaze_app python manage.py startapp account创建完 App 后需要去settings.py的INSTALLED_APPS里把dehaze_app和account加进去同时配置数据库连接和静态文件目录。这一步漏掉是新手最常见的翻车点——App 没注册跑makemigrations时系统根本找不到模型。3.2 视图层怎么接文件、调算法、写记录核心视图只需要两个一个处理上传和去雾一个展示历史记录。上传视图的职责是接收request.FILES里的图片 → 校验格式 → 存一份原图到media目录 → 调用dehaze()处理 → 把结果图也落盘 → 把任务记录写进 MySQL → 跳转到结果页。import os import uuid from datetime import datetime from django.conf import settings from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from .models import DehazeTask from .dehaze import dehaze import cv2 login_required def upload_and_dehaze(request): if request.method POST: image_file request.FILES.get(image) if not image_file: return render(request, upload.html, {error: 请选择图片}) # 1. 校验扩展名防止上传非图片文件 ext os.path.splitext(image_file.name)[1].lower() if ext not in (.jpg, .jpeg, .png, .bmp): return render(request, upload.html, {error: 仅支持 jpg/png/bmp 图片}) # 2. 保存原图到 media/origin/ 下文件名用 uuid 避免冲突 origin_path os.path.join(settings.MEDIA_ROOT, origin) os.makedirs(origin_path, exist_okTrue) unique_name f{uuid.uuid4().hex}{ext} origin_full os.path.join(origin_path, unique_name) with open(origin_full, wb) as f: for chunk in image_file.chunks(): f.write(chunk) # 3. 调用算法处理 img cv2.imread(origin_full) result_img dehaze(img) # 4. 保存结果图 result_dir os.path.join(settings.MEDIA_ROOT, result) os.makedirs(result_dir, exist_okTrue) result_full os.path.join(result_dir, unique_name) cv2.imwrite(result_full, result_img) # 5. 写 MySQL 记录 task DehazeTask.objects.create( userrequest.user, origin_imageforigin/{unique_name}, result_imagefresult/{unique_name}, algorithm_nameDarkChannelPrior, created_atdatetime.now(), ) return redirect(result_page, task_idtask.id) return render(request, upload.html)要注意两个细节第一request.FILES拿到的文件对象必须用chunks()分块写入不要一次性read()到内存大图会被撑爆第二cv2.imwrite写中文路径会失败所以文件名全部改用uuid彻底绕开编码问题。settings.MEDIA_ROOT在settings.py里要配好同时MEDIA_URL /media/也要设置否则网页上img src...无法访问到这些文件。3.3 结果页展示处理前后对比图结果页要把原图和去雾图放在一起配合任务信息展示。这里有个容易踩的坑Django 模板里引用media文件时如果MEDIA_URL没配对图片会 404。模板里比较稳妥的写法是{% if task %} div classcompare-box div classitem h3原图/h3 img src{{ MEDIA_URL }}{{ task.origin_image }} alt原图 /div div classitem h3去雾结果/h3 img src{{ MEDIA_URL }}{{ task.result_image }} alt去雾结果 /div p算法{{ task.algorithm_name }} | 处理时间{{ task.created_at }}/p /div {% endif %}模板里直接用{{ MEDIA_URL }}的前提是在settings.py里配好MEDIA_URL并且在urls.py中用static()辅助函数挂载了 media 目录。另一个高频问题是历史记录页面查询出多条记录时Django 默认懒加载外键会导致 N1 查询每显示一条记录就多查一次用户表。这时要用到select_relatedfrom django.views.generic import ListView from .models import DehazeTask class HistoryView(ListView): model DehazeTask template_name history.html paginate_by 10 def get_queryset(self): # select_related 一次 join 查出 user避免 N1 查询 return (DehazeTask.objects .filter(userself.request.user) .select_related(user) .order_by(-created_at))这个列表页上的分页器、日期显示、图片缩略图构成了“完整前后端”里前端最直观的部分。不需要写复杂 JavaScriptDjango 模板 简单的 CSS 就够一个毕设系统用了。4. MySQL 数据模型设计用户、图片任务和算法记录怎么建表4.1 两张核心表的结构与设计理由图像去雾系统的数据模型其实很简单核心就是“用户”和“去雾任务”两张表。任务表通过外键关联用户表一个用户可以有多个任务这就是一对多关系。建表时要注意字段类型图片路径存varchar不存二进制大对象算法参数存 JSON 字符串方便以后扩展状态字段用tinyint0 表示处理中、1 表示成功、2 表示失败。-- 用户表 CREATE TABLE account_user ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password_hash varchar(128) NOT NULL, email varchar(100) DEFAULT NULL, is_active tinyint(1) NOT NULL DEFAULT 1, date_joined datetime(6) NOT NULL, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 去雾任务表 CREATE TABLE dehaze_app_dehazetask ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, origin_image varchar(255) NOT NULL, result_image varchar(255) NOT NULL, algorithm_name varchar(50) NOT NULL DEFAULT DarkChannelPrior, params_json longtext, status tinyint(1) NOT NULL DEFAULT 0, created_at datetime(6) NOT NULL, finished_at datetime(6) DEFAULT NULL, PRIMARY KEY (id), KEY user_id (user_id), CONSTRAINT fk_task_user FOREIGN KEY (user_id) REFERENCES account_user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表名要跟 Django 模型的Meta类里db_table对应不然迁移时会创建新的默认表名跟已有 SQL 脚本对不上。字符集统一用utf8mb4因为utf8在 MySQL 里存不了 Emoji虽然用户名一般用不到但统一总归省心。4.2 用 django 的 models.py 描述同一套结构写 Django 的models.py时要跟上面的 SQL 保持字段一致。这样既可以用 Django 的 ORM 做查询迁移也可以理解底层表结构——答辩时老师大概率会问“任务表为什么要有status字段”。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): 扩展 Django 默认用户模型保留 username/password 之外加手机号等可选字段 phone models.CharField(max_length20, blankTrue, nullTrue, verbose_name手机号) class Meta: db_table account_user class DehazeTask(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name所属用户) origin_image models.CharField(max_length255, verbose_name原图路径) result_image models.CharField(max_length255, verbose_name结果图路径) algorithm_name models.CharField(max_length50, defaultDarkChannelPrior, verbose_name算法名称) params_json models.TextField(blankTrue, nullTrue, verbose_name算法参数JSON) status models.BooleanField(defaultFalse, verbose_name是否完成) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) finished_at models.DateTimeField(nullTrue, blankTrue, verbose_name完成时间) class Meta: db_table dehaze_app_dehazetask ordering [-created_at]ForeignKey(on_deletemodels.CASCADE)表示用户被删除时他的去雾任务一并删除这是最常见的级联策略。ordering [-created_at]让所有查询默认按创建时间倒序历史记录页不用每次写order_by。4.3 外键查询优化select_related 的对应 SQL在历史记录页里每一条任务记录都要显示用户名。如果不做优化查 10 条记录会执行 1 条任务查询 10 条用户查询这是典型的 N1 问题。select_related会把 JOIN 提前到一次查询完成对应的 SQL 大致是SELECT * FROM dehaze_app_dehazetask INNER JOIN account_user ON dehaze_app_dehazetask.user_id account_user.id WHERE dehaze_app_dehazetask.user_id 1 ORDER BY dehaze_app_dehazetask.created_at DESC LIMIT 10;除了select_related处理一对多查询还有prefetch_related处理多对多查询。在本系统里一个任务只属于一个用户用select_related就够了。把这条区别讲清楚答辩时是一个加分点。5. 跑通系统的避坑清单从环境到数据库的 5 个典型翻车现场5.1 Django 版本与 MySQL 驱动不兼容现象执行python manage.py runserver后访问登录页正常但一注册用户就报django.db.utils.OperationalError: (2059, Authentication plugin caching_sha2_password cannot be loaded)。原因MySQL 8.0 默认加密插件是caching_sha2_password而老版本 PyMySQL 或 mysqlclient 不支持这个协议。解决装新版本驱动并在settings.py的数据库配置里加OPTIONS: {charset: utf8mb4}。如果还报错就用requirements.txt锁定版本组合。常见组合是pip install django4.2 pymysql1.1.0 cryptography然后在项目__init__.py里加两句import pymysql pymysql.install_as_MySQLdb()5.2 media 文件在模板里 404现象上传图片成功后结果页img标签地址是/media/origin/xxxx.jpg浏览器直接 404。原因开发环境没挂载 media 目录。Django 的runserver默认只处理静态文件不处理 media 文件。解决在根urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns [ # 其他路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)5.3 cv2.imread 读不到中文路径图片现象图片上传成功算法也没报错但处理结果是一张纯黑图。原因OpenCV 的imread在 Windows 上不支持中文路径静默失败返回None转成空数组后处理结果全黑。解决上传的图片一律用uuid重命名为纯英文文件名同时在测试阶段用os.path.exists加上断言检查assert os.path.exists(origin_full), f文件不存在: {origin_full} img cv2.imread(origin_full) assert img is not None, 图片读取失败检查路径是否含中文5.4 上传大图导致内存溢出或超时现象上传一张 5000x3000 的高清雾图页面转圈很久最后 502或者内存直接吃满。原因暗通道算法里np.argsort(dark_flat)对 5000x3000 的图要排 1500 万个元素耗时和内存都很夸张。同时 Django 默认上传大小没有硬限制但 Web 服务器有超时时间。解决前端用acceptimage/*过滤类型后端在视图中限制图片尺寸和文件大小。文件超过 5MB 或像素超过 2500 万就直接拒绝。算法侧可以先把长边压缩到 1600 像素再处理肉眼基本看不出差别。MAX_PIXELS 2500 * 10000 # 2500 万像素 MAX_FILESIZE 5 * 1024 * 1024 # 5MB if image_file.size MAX_FILESIZE: return render(request, upload.html, {error: 图片不能超过 5MB})5.5 注册完 App 忘记迁移数据库没有表现象python manage.py runserver不报错但登录反而提示no such table: dehaze_app_dehazetask。原因Django 不会自动建表。改了models.py之后必须执行makemigrations再migrate。如果是直接导入别人的 SQL 脚本要确认脚本里的表名跟models.py里db_table完全一致。解决python manage.py makemigrations dehaze_app account python manage.py migratemigrate之后用python manage.py showmigrations检查是否全部打勾。如果中途改过字段直接migrate可能报冲突这时可以python manage.py migrate dehaze_app zero回退后重新迁移——这是后悔药但会丢表数据开发阶段无所谓有正式数据时谨慎使用。6. 把系统做成能答辩的完整项目定量评估、说明文档和 PPT 怎么组织系统跑通只是第一步毕设评分的差距往往在“能不能讲清楚效果”。去雾算法最容易被问的是“你怎么证明你的算法有效”。单说“看起来清晰了”不够需要定量指标。实践中最常用的两个指标是 PSNR峰值信噪比和 SSIM结构相似性。PSNR 越高表示像素还原越接近参考图SSIM 越接近 1 表示结构信息保留越好。计算时用 OpenCV 自带的函数就行。import cv2 def calc_metrics(foggy_path, result_path, clear_path): foggy cv2.imread(foggy_path) result cv2.imread(result_path) clear cv2.imread(clear_path) psnr_foggy cv2.PSNR(foggy, clear) psnr_result cv2.PSNR(result, clear) ssim_result cv2.compareSSIM(result, clear, fullFalse) print(f原图 PSNR: {psnr_foggy:.2f} dB) print(f去雾后 PSNR: {psnr_result:.2f} dB提升 {psnr_result - psnr_foggy:.2f} dB) print(fSSIM: {ssim_result:.4f})需要先准备成对的质量评估数据也就是同一场景的雾图和清晰图。公开数据集常见的是 RESIDE 标准数据集里的合成雾图有对应的 ground truth 清晰图。拿 20 张图算一组平均指标然后把“处理前 PSNR/处理后 PSNR”的对比表截图放进 PPT比任何渲染图都有说服力。关于 LW论文和 PPT 的整理顺序我一般遵循“先做图表再写文字”的顺序。先把上传页截图、历史列表截图、去雾对比图、指标对比表这四类图准备好再按“研究背景 → 算法原理 → 系统设计 → 实验结果 → 总结展望”的结构填充文字。论文里放算法的公式推导时建议把暗通道定义的公式单独拎出来解释两句不要直接整段公式堆上去——答辩老师的耐心有限他能看到你理解了自己的公式比看到你背了十页推导更关键。最后说一个我自己的教训很多毕设翻车不在功能上而在环境复制上。明明代码是好的换了电脑跑不起来。所以交付前一定要把requirements.txt锁好版本把README.md写完整包括 Python 版本、Django 版本、MySQL 初始化脚本怎么执行、测试账号密码是什么。这套系统真正做完时你会发现自己花在调试环境上的时间可能比写算法还多而提前把这些写进说明文档的人在答辩现场是真的能从容很多。希望帮到你。本文还有配套的精品资源点击获取