基于YOLOv11与DeepSeek的水下鱼类检测系统设计与实现

发布时间:2026/10/17 12:09:28
基于YOLOv11与DeepSeek的水下鱼类检测系统设计与实现
这套水下鱼类检测系统我前不久刚把最终版跑通现在整理一下完整的设计思路和踩坑过程直接分享出来。项目整体是用YOLOv11做视觉检测Django做Web后端DeepSeek做鱼类知识问答另外还带登录注册、ONNX模型推理和一套打磨过的Web界面。简单说它的能力打开网页登录进去上传一张水下拍摄的鱼群照片后端先用YOLOv11把画面里的鱼逐条框出来识别鱼种和置信度再把结果展示在页面上同时把这些结构化信息交给DeepSeek生成一段关于该鱼种的习性、分布、养殖价值的科普介绍。如果你想追问比如“这种鱼在水温多少度时长得最快”系统能保持上下文继续回答。整套东西涉及目标检测、Web后端、API对接和前端交互链路完整比较适合做课程设计、毕业设计也适合想系统过一遍这套技术栈的人参考。1. 为什么做这个项目以及技术栈怎么定下来的1.1 一个“检测对话”组合需求最开始的需求其实很简单某水产养殖相关的模拟项目X想做一个水下鱼群统计工具能够对着水下摄像头拍到的画面自动数出有多少条鱼、主要是什么品种。但做着做着发现光有“检测框”不够因为客户真正想要的是“这条鱼到底是什么值不值得养适合什么环境”。这就引出了第二个需求对检测到的鱼种做一个自然语言层面的科普问答。所以这个项目本质上不是单纯的目标检测Demo而是“视觉检测 Web服务 大模型对话”的组合系统。很多初学者容易忽略需求背后的真实使用场景导致做出来的东西只是一个孤立的模型脚本没法给别人用。我在这套项目里坚持加了登录界面和Web封装就是这个原因只有跑在一个完整的Web服务里检测模型才算真正落地。1.2 YOLOv11、Django、DeepSeek各自承担什么角色选型是综合考虑的结果不是哪个新就选哪个。下面这个表基本反映了我当时的取舍逻辑技术组件承担角色选择理由YOLOv11鱼体检测与分类速度和精度均衡小目标检测里有优势官方工具链成熟ONNX Runtime推理引擎导出ONNX模型后可脱离PyTorch环境运行部署体积小CPU也能跑DjangoWeb后端与用户体系自带Auth、CSRF、ORM省去大量底层工作适合快速搭建完整系统DeepSeek鱼类知识问答中文理解能力强API接入简单上下文保持效果好成本相对可控Bootstrap 原生JS前端界面不额外引入重型前端框架后端模板渲染即可完成维护成本低Django在这套组合里其实是“黏合剂”。YOLOv11负责视觉理解DeepSeek负责语言理解Django把两者的输出汇总成用户能直接使用的服务。有人可能想用Flask但当你还需要用户登录、会话管理、后台管理这些功能时Django内置的那套东西能节省大量时间。1.3 整体模块划分整个系统我分成四个模块账户模块注册、登录、会话保持用户未登录不能访问检测和对话功能。检测模块接收上传图片调用ONNX模型推理返回检测框坐标、类别和置信度。对话模块把检测结果包装成Prompt请求DeepSeek API返回科普内容并维护多轮对话上下文。展示模块负责前端页面、结果渲染、图片标注展示。模块之间用Django的app拆分逻辑清晰。后面每一层出问题都能单独排查不至于牵一发而动全身。2. YOLOv11 模型训练与 ONNX 导出部署2.1 数据从哪里来怎么标注水下鱼类检测最难的不是模型是数据。水下照片普遍存在偏色、模糊、遮挡和鱼群密集的问题公开数据集里的干净图片和真实水下场景差距很大。我当时的做法是以某个公开的水下鱼类数据集为基础再补充一部分自己模拟水下环境拍摄和网络公开渠道收集的图片合计筛出大概8000多张有效图片。标注用的是LabelImg工具标注类别先控制在10个常见鱼种包括鲤鱼、鲫鱼、鲈鱼、带鱼、金枪鱼等。不同品种之间有相似外观所以标注时我特别要求每个框尽量贴合鱼的轮廓不要多框进背景。数据增强环节有一条重要经验因为水下图片普遍偏蓝绿色如果你做颜色增强太激进比如大幅随机调节色相饱和度反而会让模型误把“水色”当成特征。我最终只用了水平翻转、轻微旋转、缩放和亮度微调没有做极端的颜色扰动。模型要学的应该是鱼本身的外观而不是水体的颜色。2.2 训练参数与模型选型模型我用了YOLOv11的s版本也就是yolo11s。n版本更快但精度在密集鱼群上不够用m版本又太重在Web后端的CPU推理场景下会拖慢响应。s是在精度和速度之间比较稳的一个选择。训练命令大概这样pip install ultralytics yolo detect train datafish.yaml modelyolo11s.pt epochs120 imgsz640 batch16fish.yaml里就是数据集路径和类别列表。这里有个不太起眼但很关键的参数imgsz我直接定为640没有用更大的尺寸。原因是这个项目最终要导出ONNX部署部署时也按640输入。训练和部署输入尺寸保持一致能避免精度损失。epochs设了120但我加了早停机制patience设为20。实际训练到第100轮左右就收敛了验证集上的mAP50大概在0.86到0.89之间。密集鱼群场景下这个数字够用了。2.3 ONNX 导出与推理封装为什么要转ONNX因为最终系统要跑在Django服务里不能要求服务器安装完整的PyTorch和Ultralytics环境。ONNX模型体积小用onnxruntime做推理部署方便CPU环境也能跑。导出命令很简单yolo export modelbest.pt formatonnx dynamicTrue simplifyTrue导出后得到一个best.onnx关键的推理封装我写成了一个类方便Django里调用import cv2 import numpy as np import onnxruntime as ort class FishYOLO: def __init__(self, onnx_path, input_size640, conf_thresh0.45, iou_thresh0.5): self.session ort.InferenceSession( onnx_path, providers[CPUExecutionProvider] ) self.input_size input_size self.conf_thresh conf_thresh self.iou_thresh iou_thresh self.class_names [crucian, carp, perch, hairtail, tuna] def letterbox(self, img): h, w img.shape[:2] scale min(self.input_size / w, self.input_size / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(img, (nw, nh)) canvas np.full((self.input_size, self.input_size, 3), 114, dtypenp.uint8) y0 (self.input_size - nh) // 2 x0 (self.input_size - nw) // 2 canvas[y0:y0nh, x0:x0nw] resized return canvas, scale, x0, y0 def predict(self, img): input_data, scale, x0, y0 self.letterbox(img) rgb input_data[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob np.expand_dims(rgb, axis0) outputs self.session.run(None, {self.session.get_inputs()[0].name: blob})[0] # outputs shape: (1, 4num_classes, 8400) boxes outputs[0].transpose([1, 0]) # (8400, 4num_classes) cx boxes[:, 0] cy boxes[:, 1] w boxes[:, 2] h boxes[:, 3] # 还原到原图坐标 x1 ((cx - w / 2) - x0) / scale y1 ((cy - h / 2) - y0) / scale x2 ((cx w / 2) - x0) / scale y2 ((cy h / 2) - y0) / scale # 过滤 NMS # 具体NMS可直接用 cv2.dnn.NMSBoxes 处理 return results这个类有几个细节要注意。一个是letterbox的还原计算如果漏掉x0/y0偏移量框的位置会整体偏移。另一个是输入图像要转成RGB因为cv2读出来默认是BGR格式直接用会掉精度。NMS部分我直接用cv2.dnn.NMSBoxes处理省去手写非极大值抑制的麻烦。3. Django 后端登录鉴权与检测接口3.1 登录与注册逻辑Django的django.contrib.auth模块本身就提供了完整的用户认证能力所以我没有重复造轮子。用户在注册页输入用户名、邮箱、密码后台用内置的User模型创建用户登录时用authenticate和login处理会话。核心视图大概是这样from django.contrib.auth import authenticate, login from django.shortcuts import render, redirect def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return redirect(detect_page) else: return render(request, login.html, {error: 用户名或密码错误}) return render(request, login.html)注册视图类似用内置的User.objects.create_user创建用户密码会自动哈希存储。对这类系统来说密码安全交给Django内置处理就够了。有一点我自己踩过坑注册以后最好直接自动登录并跳转到主页不要让用户注册完再回登录页输一遍密码。这个小细节很影响体验。3.2 检测接口设计检测接口我设计成一个独立的Django app路由是/detect/只接受POST请求而且必须要登录。核心是用login_required装饰器控制权限。接口接收表单上传的图片文件校验扩展名和大小然后保存到内存或临时目录调用FishYOLO推理最终返回JSON{ code: 0, image_url: /media/result_20240512_001.jpg, fish: [ {bbox: [120, 84, 260, 310], class: perch, conf: 0.92}, {bbox: [300, 150, 420, 290], class: tuna, conf: 0.87} ], total: 2 }前端拿到这个JSON后用Canvas在图片上画检测框而不是让后端把图画好再返回给前端。这样做的灵活性更高用户可以自己控制框的颜色和显示逻辑。接口还做了一步把检测结果和原图路径写进数据库。数据库里记录这次检测的时间、用户、结果摘要。为什么要记录一是后面做历史记录功能需要二是排查问题时有迹可循不然只凭一张嘴说“模型出错了”根本没法定位。3.3 会话与权限控制Django默认的session是存在数据库里的对开发环境来说很方便但生产环境建议换成Redis或其他内存缓存否则每次请求都查session表并发起来容易拖慢响应。我用login_required保护所有需要登录才能访问的视图。另外还做了一个小改动未登录用户访问页面时不只是简单重定向到登录页而是把登录页弹出来登录成功后自动跳回刚才想访问的地址。这个逻辑用Django的next参数实现。权限控制表大概这几种情况访问对象匿名用户登录用户登录/注册页允许不允许检测页面禁止跳到登录允许检测API返回401允许历史记录页面禁止跳到登录允许4. 接入 DeepSeek把鱼类检测结果变成科普问答4.1 为什么检测之后还要接一个大模型只做YOLOv11检测系统输出只是“这里有一条鱼置信度92%”这不是用户最终想要的信息。用户想知道的是这条鱼的生活习性、经济价值、养殖难度。这类知识不好全部提前写死在代码里于是接入DeepSeek来生成。准确说我这里的用法是“结构化检测结果 大模型知识增强”。YOLOv11负责识别是什么鱼DeepSeek负责给出关于这种鱼的进一步说明两者结合系统价值完全不同。4.2 接口调用与密钥安全DeepSeek的API是OpenAI兼容格式调用起来很方便。我直接用requests封装不需要额外安装SDKimport os import requests DEEPSEEK_API_KEY os.getenv(DEEPSEEK_API_KEY) DEEPSEEK_API_URL https://api.deepseek.com/chat/completions def ask_deepseek(messages): headers { Authorization: fBearer {DEEPSEEK_API_KEY}, Content-Type: application/json } payload { model: deepseek-chat, messages: messages, temperature: 0.8 } resp requests.post(DEEPSEEK_API_URL, headersheaders, jsonpayload, timeout30) data resp.json() return data[choices][0][message][content]密钥管理是这里的一个重点。我在settings.py里通过环境变量读密钥绝对不写死在源码里也不让前端拿到。如果你提交代码到公开仓库密钥一旦泄露别人就能无限调用你的API额度这个教训我是实打实吃过亏的。4.3 Prompt 设计与多轮对话Prompt设计直接决定大模型回答质量。我的做法分两步先把检测结果转成一段结构化文字再拼进系统Prompt里系统角色你是水下鱼类科普专家。下面是一次自动检测得到的结果请你对结果进行总结并对画面中出现的鱼类提供详细科普。 检测结果画面中共有2条鱼分别是 - 鲈鱼置信度0.92 - 金枪鱼置信度0.87 请对每种鱼给出分类地位、外形特征、生活习性、经济价值、养殖注意事项。这样设计的好处是大模型不需要“看”图片也能基于我们可信的视觉检测结果做延伸。同时前端对话框里的用户追问会作为历史消息拼接进messages数组保持多轮上下文。实测下来DeepSeek对常见养殖鱼类的知识覆盖比较全面对细小品种偶尔会给出比较泛泛的回答所以在Prompt里我加了“如果对品种不确定请直接说明不确定不要编造”的约束减少幻觉。5. 精美 Web 界面前端交互与展示细节5.1 页面结构与视觉风格前端我选择基于Bootstrap 5做整体框架再写一套自定义CSS调视觉。没有上Vue或React主要原因是不想引入前后端分离和打包构建后端模板渲染对这类工具型系统来说效率更高。页面主要分为三个登录页居中卡片布局背景做成了深蓝到青绿的渐变呼应水下主题。检测主页左侧是功能区中间是图片预览和检测结果右侧是聊天面板。历史记录页表格列出往期检测记录点开能看到当时的检测结果。视觉风格上我刻意少用鲜艳颜色整体以深色背景加亮色按钮为主检测框用荧光绿。水下场景本身是蓝色调的界面上再用大面积的绿色会显得很脏。深色背景能让检测框更突出。5.2 上传图片与实时标注前端核心交互是上传图片后立刻看到标注结果。我用fetch发送FormData不刷新页面const formData new FormData(); formData.append(image, fileInput.files[0]); const resp await fetch(/detect/, { method: POST, body: formData }); const data await resp.json(); drawBoxes(data);drawBoxes函数在Canvas上重新绘制原图再根据返回的bbox坐标画框。这里有个细节Canvas绘制时如果直接以原图尺寸画大图会撑爆页面布局。我会先按页面容器宽度计算缩放比例再等比缩放坐标。这是一个看起来很不起眼、实际体验差很多的地方。等待检测时我加了一个加载动画动画文案会分阶段变化比如“正在处理图片…”“模型推理中…”“正在生成科普信息…”让用户知道系统在干活而不是卡死了。5.3 检测报告与聊天面板右侧聊天面板是整个界面里最花心思的部分。检测完成后系统会自动把检测结果发给DeepSeek返回的科普内容直接以聊天气泡的形式出现在面板里视觉上和用户的追问消息混在一起形成“AI先汇报结果再陪用户聊天”的体验。聊天记录的存放我放在sessionStorage里刷新页面可以保留关闭浏览器自动清空。没有做数据库持久化主要是历史对话的存储对这类系统优先级不高先省掉。这个面板的DOM操作直接用原生JavaScript完成手动控制聊天区域的滚动条保证新消息出现时自动滚到底部。不做框架反而是这里最合适的选择。6. 部署全过程与高频问题排查实录6.1 初始化环境清单一套干净的环境我建议按这个顺序来python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django ultralytics onnxruntime opencv-python-headless requests pillowONNX推理我用的是onnxruntime的CPU版本。如果你有NVIDIA显卡可以换成onnxruntime-gpu推理速度会快不少但要注意GPU版依赖CUDA和cuDNN版本环境配置复杂度会上来。项目跑通优先的话先用CPU没问题。模型文件、迁移文件、密钥这些不建议放进代码仓库。model文件放单独的models目录启动脚本通过环境变量指定路径。6.2 常见问题和解决思路我把这次实际部署中遇到最多的问题整理成一张表基本覆盖了后期维护时排查的重要位置问题可能原因解决方式ONNX推理结果全是空白框输入通道BGR/RGB顺序不对推理前把cv2读的BGR转成RGB检测框位置偏移letterbox的坐标还原漏了偏移量检查x0/y0在还原时是否减掉Django接口上传图片报413请求体大小超限制settings里调DATA_UPLOAD_MAX_MEMORY_SIZE模型首次推理很慢onnxruntime创建session有初始化开销把模型类实例定义成单例只初始化一次DeepSeek接口偶尔超时API响应时间长或网络波动requests设timeout加入重试逻辑中文路径图片读取失败OpenCV读不了含中文的路径上传图片统一重命名为ASCII文件名并发请求时服务卡顿onnxruntime多线程争抢CPU资源设置sess_options.intra_op_num_threads这里最有价值的经验是“模型只加载一次”。很多人写Django视图直接在函数里ort.InferenceSession一下每次请求都重新加载模型磁盘和内存开销都很大。正确的做法是把FishYOLO实例放在模块层服务启动时创建一次整个生命周期复用。6.3 性能优化建议如果后续要把这个系统放到生产环境我建议优先做三件事。第一是图片上传前做尺寸限制。前端在客户端压缩图片后端也做一层校验超过比如1920像素宽就等比缩小。YOLOv11推理本身很快瓶颈反而在图片编解码和网络传输上。第二是把检测结果缓存起来。同一张图片短时间内重复上传直接用缓存结果返回不必重新推理。我简单用Django的cache框架做了内存缓存响应时间从几百毫秒降到几十毫秒。第三是把DeepSeek调用改成异步。当前同步请求导致用户要等模型返回科普信息才能看到完整结果。如果并发量上来建议用CeleryRabbitMQ或者简单的后台线程池处理先把图片检测结果返回给用户科普信息生成后再推送。这套改造不算复杂但能显著提升体验。写到最后的一点心里话项目完整链路跑通之后我最大的感受是这类多技术栈系统真正花时间的不是某个单独环节而是把视觉、Web、大模型三者组合在一起时出现的各种“边角问题”。模型训练本身只要数据到位两三天就能收敛Django登录更是现成组件DeepSeek接入也就几十行代码。难的是让它们顺畅地协作比如检测结果的坐标系怎么传给前端绘图比如大模型的回答怎么和检测结果保持逻辑一致。如果你也想复刻这套系统我建议按照“先本地脚本跑通检测 → 再套Django接口 → 再接DeepSeek → 最后美化页面”的顺序来做。不要一上来就同时搞所有模块否则排查问题时你会分不清是模型的问题、后端的问题还是前端的问题。后续如果要做视频流检测可以把推理逻辑抽成独立的检测服务用消息队列对接视频帧想增加鱼种只需要在数据集里加标注重新训练代码层面几乎不用动。这套系统的扩展空间还是比较大的祝你也能顺利跑通。