多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

图像去雾系统毕业设计全解析:Django+暗通道先验算法落地

图像去雾系统毕业设计全解析:Django+暗通道先验算法落地 简介一套面向毕业设计场景的图像去雾算法研究系统基于Python语言和Django框架构建完整前后端配合MySQL数据库管理数据实现了从图片上传、算法处理到结果实时展示的闭环流程。系统中集成了暗通道先验、Retinex理论、基于物理模型等经典去雾方法并设计了用户登录、图片管理、处理记录等实用模块适用于计算机视觉、图像处理方向的毕业设计或课题研究。压缩包共包含250个文件主要有Python程序文件、HTML页面、CSS与JavaScript脚本、SQL数据库脚本、项目说明文档docx、答辩PPT以及大量gif演示动图整体大小48.39MB目录层级分明可按模块快速定位。目前已有71人学习/下载资料另附详细说明文档和PPT系统阐述开发背景、系统架构设计、算法数学原理、核心代码实现和使用方法。同时配有运行效果截图与动态演示便于读者快速理解去雾流程并复现项目整体是一份结构完整、实战性强的毕业设计参考资料。1. 图像去雾算法研究系统毕业设计里那个“能跑能演示”的完整闭环拿到这个项目标题先别急着被“算法”两个字带偏。它其实是一条完整的毕业设计链路前端 HTML 做交互页面Django 做后端路由与逻辑编排MySQL 存图像处理记录和用户数据中间再嵌一个图像去雾算法模块。这套组合在本科毕设里非常典型——“有算法、有界面、有数据库”答辩时既能讲原理又能现场演示老师最吃这一套。但真正把这份源码跑起来的人十个里有七个栽在环境配置、Django 版本和 MySQL 字符集上。算法本身反而不是最难的部分不管它用的是暗通道先验、直方图均衡化还是轻量级 CNN核心逻辑通常只有几十行代码难的是让它作为“研究系统”稳定运行——上传一张带雾图页面能转圈、后台能计算出结果、结果能存进 MySQL、前端能回显去雾前后的对比。这篇文章就围绕这条链路把算法原理、Django 工程结构、数据库表设计和踩坑点全部拆开讲。适合正在做毕设、或者想快速理解这类“算法 Web 管理端”系统怎么落地的人。2. 去雾算法选型暗通道先验为什么是这类系统的默认答案2.1 暗通道先验的数学原理与适用边界去雾算法五花八门从传统的直方图均衡化、Retinex到基于深度学习的 AOD-Net、DehazeNet但大多数毕设系统最终都会落到**暗通道先验Dark Channel Prior, DCP**上。原因很简单它效果好、代码量小、论文好写而且调参空间足够撑起“研究”二字。先讲原理。对有雾图像大气散射模型写作I(x) J(x) * t(x) A * (1 - t(x))其中 I(x) 是观测到的有雾图像J(x) 是清晰图像t(x) 是透射率A 是全局大气光。去雾的目标就是根据 I(x) 反推 J(x)关键在估计 A 和 t(x)。暗通道先验的观察是对无雾图像的每个像素在 R、G、B 三个通道里取最小值再对一个局部区域做最小值滤波结果趋近于 0。也就是J_dark(x) min_{y ∈ Ω(x)} ( min_{c ∈ {R,G,B}} J_c(y) ) → 0把这个先验代入大气散射模型一通代数变换后就能解出透射率。这个计算过程非常短用 OpenCV 的cv2.erode最小值滤波加两次cv2.min就能完成。代码实现大约五十行跑一张 600×400 的图在 CPU 上耗时不超过一秒完全够实时演示。但要注意暗通道先验有两个天然的失效场景大片天空区域和白色高亮物体比如白色建筑物、车灯。因为这些区域的暗通道本来就大不满足“趋近于 0”的假设处理完会出现明显的偏色块。所以源码里通常会加一个透射率下限t0把透射率钳在 0.1~0.15 之上防止天空区域被过度增强产生灰色噪声块。后面调参数时这个t0是第一个要动的值。2.2 直方图均衡化做对比基线三行代码出效果的代价毕设系统的“研究性”体现在对比实验上。绝大多数这类项目会在算法模块里塞两到三种方法暗通道先验是主方法另外再配一个对比方法——最常见的是全局直方图均衡化HE和限制对比度自适应直方图均衡化CLAHE。全局 HE 的实现只有三行import cv2 def histogram_equalization(image): # 将 BGR 转到 YCrCb 色彩空间只对亮度通道做均衡化 ycrcb cv2.cvtColor(image, cv2.COLOR_BGR2YCrCb) ycrcb[:, :, 0] cv2.equalizeHist(ycrcb[:, :, 0]) return cv2.cvtColor(ycrcb, cv2.COLOR_YCrCb2BGR)为什么只均衡 Y 通道而不是直接在 BGR 三个通道上做因为直接对 R、G、B 分别均衡化会把色彩比例打乱结果图会偏色得非常夸张。转 YCrCb 只动亮度分量能保留原始色彩信息。这个细节在答辩时经常被问到属于加分项。CLAHE 稍微复杂一点多两个参数clipLimit对比度限制阈值和tileGridSize分块网格大小。CLAHE 的优势是对局部细节的增强比全局 HE 温和不太会出现大片过曝区域缺点是参数敏感分块太小会出现格子状伪影。这套双方法对比的结构对应到项目里通常是一个algorithms/目录里面放dcp.py、he.py、clahe.py三个独立模块统一暴露一个process(image_path, params) - output_path接口。Django 视图层不关心算法内部细节只调用接口取结果。2.3 透射率 t0 和窗口半径两个必调参数的经验取值暗通道先验的实现里有两个参数直接决定最终效果一个是前面提到的透射率下限t0另一个是暗通道计算时的滤波窗口半径radius。radius控制最小值滤波的窗口大小。窗口开得越大暗通道越“平滑”估计出的透射率越均匀但边缘细节会丢失物体轮廓周围容易出现白边halo 效应窗口开得过小暗通道里残留噪声透射率图会很杂去雾后画面显得“脏”。经验值是窗口半径取 15~25 像素。对 600×400 的输入图radius15是一个稳妥的起点。t0的取值逻辑比较反直觉t0 设得越大去雾力度越弱。因为 t(x) 的下限越高最终恢复公式里分母max(t, t0)越大J(x) 整体偏保守。常见的取值区间是 0.08~0.2。如果用户上传的雾图属于浓雾白茫茫一片t0取 0.1 会更有冲击力如果是薄雾t0保持 0.15 以上画面过渡更自然。调参的实际体验是跑通 demo 用默认参数追求演示效果得针对雾图手动调。这也是为什么这类项目的前端页面上通常会有两个输入框让用户自己改t0和radius改完重新提交系统重新跑一遍算法。这个交互设计在答辩演示时特别好用——现场换一张带雾图调一下参数再对比观感上就是“可研究的系统”而不是“一个死代码”。3. Django 工程结构与业务逻辑算法包成 Web 服务的关键设计3.1 项目目录划分APP 拆分的常规做法拿到这份源码先花五分钟看目录结构。一个可维护的 Django 去雾系统工程布局通常长这样project_root/ ├── manage.py ├── config/ # 项目配置 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── app/ │ ├── views.py # 视图函数上传、处理、结果展示 │ ├── models.py # 数据表模型 │ ├── urls.py # 子路由 │ ├── forms.py # 上传表单 │ ├── algorithms/ # 算法包 │ │ ├── dcp.py │ │ ├── he.py │ │ └── clahe.py │ └── migrations/ ├── templates/ │ ├── base.html │ ├── upload.html │ └── result.html ├── media/ │ ├── uploads/ # 原始上传图 │ └── outputs/ # 去雾结果图 └── static/ # CSS/JS 文件这里有个重要的设计选择算法目录algorithms/是放在 app 内部、还是放在项目根目录独立成包两种都行但放 app 内部更省事。import 路径短Django 的启动流程能自动识别打包迁移也方便。独立成包的好处是算法可以脱离 Web 框架单独测试但对毕设项目来说这个收益不大反而容易因为路径配置问题在部署时报ModuleNotFoundError。media/目录是必须提前建好的。Django 不会自动创建media/uploads和media/outputs如果这两个目录不存在上传图片时会直接抛FileNotFoundError。这个坑几乎人人都会踩一遍后面避坑章节会细说。3.2 图像上传与结果回显视图层的三步走逻辑Django 视图层的核心逻辑是一条清晰的三步链路接图 → 存图 → 调算法 → 存结果 → 回传路径。代码骨架如下import os import time from django.shortcuts import render from django.conf import settings from .forms import UploadForm from .algorithms.dcp import dark_channel_prior from .algorithms.he import histogram_equalization def process_image(request): if request.method POST: form UploadForm(request.POST, request.FILES) if form.is_valid(): # 1. 构造本次任务的唯一目录避免文件名冲突 task_id str(int(time.time())) upload_dir os.path.join(settings.MEDIA_ROOT, uploads, task_id) output_dir os.path.join(settings.MEDIA_ROOT, outputs, task_id) os.makedirs(upload_dir, exist_okTrue) os.makedirs(output_dir, exist_okTrue) # 2. 保存上传文件 uploaded_file request.FILES[image] input_path os.path.join(upload_dir, uploaded_file.name) with open(input_path, wb) as f: for chunk in uploaded_file.chunks(): f.write(chunk) # 3. 读取算法参数并调用算法 method form.cleaned_data[method] t0 form.cleaned_data.get(t0, 0.1) radius form.cleaned_data.get(radius, 15) if method dcp: output_name dcp_result.jpg dark_channel_prior(input_path, output_dir, t0t0, radiusradius) else: output_name he_result.jpg histogram_equalization(input_path, output_dir) # 4. 构造相对 Web 的访问 URL result_rel foutputs/{task_id}/{output_name} return render(request, result.html, { original_url: fuploads/{task_id}/{uploaded_file.name}, result_url: result_rel, }) return render(request, upload.html, {form: UploadForm()})这段逻辑有几个值得注意的细节。第一用时间戳做任务目录。如果直接把文件存进uploads/根目录重名文件会互相覆盖而且所有记录堆在一起后期清理很麻烦。按时间戳分目录一次上传对应一个独立任务目录数据库里只需要存相对路径不需要额外生成 UUID。第二用chunks()而不是read()读文件。chunks()是 Django 在文件较大时的推荐读取方式按块写盘避免一次性把大图载入内存。虽然单张图片通常不超过 5MB但这个写法更规范不会被答辩老师挑刺。第三算法函数直接传路径而不是传图像数组。这样算法的输入输出解耦前端只管文件路径算法只管读图写图中间不经过 base64 编码省掉一坨内存开销。3.3 MySQL 表结构任务记录、算法参数与评价指标怎么落库数据库不是摆设。在这类“研究系统”里MySQL 至少承担两个职责记录每次处理任务的关键信息以及存储评价指标PSNR、SSIM 这些。如果系统只做单次上传处理、不存任何历史记录答辩时被问到“你的数据库在哪儿”就会非常尴尬。一个典型的数据表设计长这样from django.db import models class DehazeTask(models.Model): task_id models.CharField(max_length32, uniqueTrue, verbose_name任务ID) original_image models.CharField(max_length255, verbose_name原始图路径) result_image models.CharField(max_length255, verbose_name结果图路径) method models.CharField(max_length16, verbose_name去雾方法) t0 models.FloatField(default0.1, verbose_name透射率下限) radius models.IntegerField(default15, verbose_name滤波窗口半径) psnr models.FloatField(nullTrue, blankTrue, verbose_namePSNR) ssim models.FloatField(nullTrue, blankTrue, verbose_nameSSIM) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table dehaze_task verbose_name 去雾任务记录 def __str__(self): return f{self.task_id}-{self.method}字段设计上有几个考虑。task_id设置成uniqueTrue和视图层里的时间戳目录一一对应这样前端展示历史记录时可以直接通过 task_id 拼出图片 URL。method字段用字符串而不是外键因为方法就两三种没必要单独建表。t0和radius单独存是为了后续做“同一张图不同参数的效果对比”——这又是一个答辩加分项。评价指标字段的nullTrue, blankTrue是刻意的。因为用户上传的自然雾图没有“标准答案”ground truthPSNR 和 SSIM 算不出来这两个字段允许为空。但如果系统里内置了合成雾图数据集比如用cv2.add给清晰图加模拟雾就能算出参考指标此时两个字段就有值了。4. 本地跑通的最小步骤从 Python 环境到页面出图4.1 环境安装与依赖版本Python 3.8 和 Django 2.2 的兼容性拿到源码先别看算法先看requirements.txt。这类项目最常见的版本组合是Python 3.6~3.8 Django 2.2.x mysqlclient 1.4.x opencv-python 4.1.x。这个组合是 2020 年前后毕业设计项目的“标准套餐”matplotlib、numpy、scikit-image 作为算法依赖一并装。一个很重要的实操建议别用最新的 Python 3.11 或 3.12 去跑这份源码。原因有两条第一opencv-python 在 Python 3.10 的某些旧版本上有二进制兼容问题第二Django 2.2 在 Python 3.8 上跑得最稳到了 3.9 以上部分 API 已经有 deprecation 警告。如果电脑里同时有多个 Python 版本用 conda 建一个 py38 环境最省心conda create -n dehaze python3.8 conda activate dehaze pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple依赖装完后先做一步快速验证——确认关键包能正常导入import django import cv2 import MySQLdb print(django.get_version(), cv2.__version__)如果MySQLdb导入报错说明 mysqlclient 没装好或者版本不兼容这是接下来最常见的拦路虎。4.2 settings.py 里必须改对的三项配置打开settings.py需要重点核对三项配置缺一个都跑不起来。第一项是数据库配置。源码里默认的 MySQL 连接信息通常是这样DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: dehaze_db, USER: root, PASSWORD: 123456, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }这里要踩的坑在NAME对应的数据库必须预先在 MySQL 里创建好。Django 的migrate不会帮你创建数据库它只建表。所以要手动执行CREATE DATABASE dehaze_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;charset配置成utf8mb4而不是utf8是因为 utf8 在 MySQL 里不是真正的 utf8它不支持四字节的 Emoji 字符。虽然本次系统的数据不会存 Emoji但万一以后扩展了用户评论功能会直接踩这个坑。一劳永逸用 utf8mb4。第二项是静态文件和媒体文件配置。检查settings.py尾部有没有这个配置块MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media) STATIC_URL /static/ STATICFILES_DIRS [os.path.join(BASE_DIR, static)]没有的话必须补上。MEDIA_ROOT指向的是文件系统里的绝对路径MEDIA_URL是浏览器访问时的 URL 前缀。前面视图代码里用settings.MEDIA_ROOT拼接路径如果这个配置缺失运行时会直接报 AttributeError。第三项是ALLOWED_HOSTS。调试模式下改成ALLOWED_HOSTS [*]后面如果用python manage.py runserver 0.0.0.0:8000跑在局域网里让手机或另一台电脑访问ALLOWED_HOSTS不配置对应地址会直接拒绝请求。4.3 数据库迁移、管理员创建与页面访问流程配置改完后按顺序执行初始化命令# 1. 生成迁移文件 python manage.py makemigrations # 2. 写入数据库建表 python manage.py migrate # 3. 创建管理员账号 python manage.py createsuperuser # 4. 启动开发服务器 python manage.py runserver 0.0.0.0:8000makemigrations和migrate的区别经常被搞混。前者是扫描 models.py 的变更生成迁移脚本后者是真正把迁移脚本执行到数据库里。每次改了models.py里的字段都要重跑这两步。启动成功后浏览器访问http://127.0.0.1:8000首页应该显示上传表单。这时候可以准备一张带雾图试跑选一张户外实景雾图网上搜“foggy road”或“雾天街道”别选纯风景的合成雾图真实感强一些算法处理前后对比效果越明显。如果页面没起来先去终端看报错。常见的两类错误一类是 MySQL 密码不匹配Access denied for user一类是ModuleNotFoundError缺某个依赖。这两类问题在避坑章节里展开。5. 避坑与排查从上传白屏到效果灰蒙系统的五处大坑5.1 坑一上传图片后页面直接白屏终端报错FileNotFoundError现象上传表单能打开但一提交就白屏终端里抛FileNotFoundError: [Errno 2] No such file or directory: media/uploads/...。原因Django 不会自动创建 media 目录。源码里media/目录往往在压缩包里是空的或者压根不存在解压后目录丢失。解决手动在项目根目录下建好media/uploads/和media/outputs/两个目录然后在settings.py确认MEDIA_ROOT指向了正确路径。想彻底避免这个坑可以在视图代码里os.makedirs(upload_dir, exist_okTrue)之前额外加一行os.makedirs(settings.MEDIA_ROOT, exist_okTrue)。5.2 坑二ModuleNotFoundError: No module named MySQLdb现象runserver直接起不来报错信息指向django.db.backends.mysql。原因系统里没有安装 MySQL 的 Python 驱动。Django 连 MySQL 在 Python 3 环境靠 mysqlclient而不是 MySQL-python那已经是 py2 时代的包名。解决pip install mysqlclient。如果在 Windows 上安装报编译错误先装wheel包或者直接改用.whl离线安装。实在装不上 mysqlclient 时退路是换成 PyMySQL把引擎改成django.db.backends.mysql并在__init__.py里加import pymysql; pymysql.install_as_MySQLdb()这是比较常见的临时替代方案但注意尽量不要和新装的 mysqlclient 混用。5.3 坑三去雾效果图灰蒙蒙画面像盖上脏抹布现象算法跑通但输出图整体偏灰暗部细节糊成一片天空区域还有奇怪的灰斑。原因透射率下限t0设得太低或者暗通道窗口半径radius太小。t0 低于 0.05 时无雾区域的透射率趋近于 0恢复公式里除法放大噪声radius 低于 5 时暗通道不够平滑透射率图噪声密集。解决把t0从 0.05 往 0.1~0.15 调radius从 5 往 15~20 调。调完重跑一次对比灰蒙感会立刻减轻。另一个隐形因素如果输入图本身有严重的 JPEG 压缩噪点先给图片做一次轻度的cv2.GaussianBlur再去雾效果更干净。5.4 坑四页面能出结果但图片加载不出来404现象结果页显示出来了但img标签的图片裂开浏览器控制台报 404。原因Django 在调试模式下不托管 MEDIA 目录的文件。默认的 urls.py 里没有为 media 文件配置路由。解决在项目的config/urls.py里加一段from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)注意这只适用于DEBUGTrue的开发模式。部署到服务器上时media 文件的托管得交给 Nginx这个很多人到答辩前才发现白白耽误一晚上。还有一个更容易被忽略的细节static()函数拼接时如果MEDIA_URL以斜杠开头但没有结尾斜杠路径会变成/mediauploads/...首页加不加斜杠跟着settings.py里MEDIA_URL的取值一致即可。5.5 坑五管理员后台打不开或者打开了看不到任务记录现象http://127.0.0.1:8000/admin/登录后页面空白或者列表里没有DehazeTask这张表的数据。原因源码里可能漏掉了admin.py注册或者 models.py 没写__str__方法。解决在 app 的admin.py里显式注册表格from django.contrib import admin from .models import DehazeTask admin.register(DehazeTask) class DehazeTaskAdmin(admin.ModelAdmin): list_display [task_id, method, t0, radius, psnr, ssim, create_time] list_filter [method, create_time]list_display控制后台列表页展示的列list_filter提供右侧筛选。这样每次跑完算法后台里能看到一条带参数字段的记录答辩时打开后台演示说服力直接上一层。6. 进阶从“跑通”到“能说清”SSIM/PSNR 指标加一个参数调优页源码能跑通只是起点毕业设计的评分差距通常在“研究深度”上拉开。这里给两个投入产出比最高的进阶方向给系统加评价指标计算以及做一张参数对照表页面。评价指标在图像去雾领域用得最多的是 PSNR峰值信噪比和 SSIM结构相似性。但前提是得有“标准答案”——一张清晰的参考图。实操里可以这样做先找一张无雾清晰图然后用合成雾生成一张带雾图作为系统输入这样算法输出就能和原图计算指标。import cv2 import numpy as np def add_synthetic_fog(image, intensity0.6): # 模拟均匀大气光散射 fog np.full_like(image, 255, dtypenp.uint8) return cv2.addWeighted(image, 1 - intensity, fog, intensity, 0) def calc_metrics(result_path, gt_path): result cv2.imread(result_path) gt cv2.imread(gt_path) psnr cv2.PSNR(result, gt) ssim_value ssim(result, gt) # 来自 skimage.metrics return psnr, ssim_valueadd_synthetic_fog里的intensity是雾浓度0.6 对应中等浓度的雾。这样生成的雾图喂给系统后在结果页面上同时展示 PSNR 和 SSIM 数值答辩时直接读出“PSNR 提升 12dBSSIM 从 0.43 提升到 0.86”比任何口头论证都有力。参数对照表页面的实现思路是在结果页面加两个输入框t0和radius多提交几次数据库里就有多条同一张原始图、不同参数的记录。后台新增一个“参数对比”功能把同一original_image的记录按radius排序展示效果一目了然。这个功能只要复用已有的DehazeTask表和模板循环就能做技术门槛很低但视觉冲击很强。整个系统跑顺之后我的个人习惯是——先跑默认参数存一条记录再调一次t00.15, radius20存一条记录对比两次记录的 PSNR 数值。如果数值变差不要急说明算法本身对这张图的最优参数就在默认值附近这也是研究中一个正常结论。真正让人建议放弃某个参数组合的信号是结果图出现肉眼可见的颜色断裂带这时候优先检查radius是不是超过了图片短边的十分之一。暗通道先验这个方向上限不高但用来做研究系统的引擎足够扎实了。把这个流程完整走一遍你会对这个标题里的每个组件都建立起实打实的体感而不是停留在“解压后跑不起来”的焦虑里。希望帮到你。本文还有配套的精品资源点击获取
返回列表