前言

  📅大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科同学来说是充满挑战。为帮助大家顺利通过和节省时间与精力投入到更重要的就业和考试中去,学长分享优质的选题经验和毕设项目与技术思路。

  🚀对毕设有任何疑问都可以问学长哦!

   选题指导:

  最新最全计算机专业毕设选题精选推荐汇总

  大家好,这里是海浪学长毕设专题,本次分享的课题是:

  🎯基于图像处理的的海洋渔场鱼类多目标检测系统算法研究

最后

选题意义背景

海洋牧场作为一种新型渔业生产模式,近年来在全球范围内得到快速发展。随着养殖规模扩大和密度增加,如何实现高效的鱼类监测和管理成为海洋牧场可持续发展的关键。传统的人工观测和抽样调查方法不仅效率低下,而且难以全面反映鱼类活动状态和数量变化,无法满足现代海洋牧场精准管理的需求。水下目标检测技术为海洋牧场鱼类监测提供了新的解决方案。然而,水下环境复杂多变,光照条件差,海水对光线的吸收和散射作用导致水下图像普遍存在颜色失真、对比度低和细节模糊等问题。同时,鱼类活动频繁,常常出现相互遮挡和与背景颜色相似等情况,增加了目标检测的难度。现有的目标检测算法在水下环境中的表现往往不尽如人意,检测精度和召回率有待提高。
在这里插入图片描述

深度学习技术的快速发展为解决水下目标检测难题提供了新的思路。基于深度学习的目标检测算法,特别是YOLO系列算法,在陆上环境中取得了优异的性能,但直接应用于水下环境时仍面临诸多挑战。因此,针对水下环境特点,改进深度学习目标检测算法,提高其在水下环境中的适应性和准确性,具有重要的理论意义和应用价值。
本研究旨在开发一套基于深度学习的海洋牧场鱼类目标检测系统,通过改进图像增强算法和目标检测网络,提高水下鱼类目标检测的准确性和鲁棒性,为海洋牧场的智能化管理提供技术支持,促进海洋渔业的可持续发展。

数据集

URPC数据集

URPC2021数据集是2021年全国机器人大赛公开的水下目标数据集,包含多种海洋生物图像。该数据集涵盖了海胆、海参、贝类和海星等常见海洋生物,图像数量充足,种类多样,为水下目标检测算法的训练和测试提供了良好的数据基础。在本研究中,我们对URPC2021数据集进行了重新标注,输出为YOLO格式,以便用于目标检测网络的训练。数据集按照训练集:验证集:测试集=7:2:1的比例进行划分,确保训练和评估的科学性。

WEEVER海鲈鱼数据集

为了更贴近实际海洋牧场养殖场景,本研究还从青岛鲁海丰海洋牧场采集了海鲈鱼图像,构建了WEEVER海鲈鱼数据集。原始数据集包含张分辨率为1600×900的PNG格式图像。考虑到数据样本较少可能导致模型过拟合,我们采用了多种数据扩充方法,包括仿射变换、添加噪声和随机明度、色相、饱和度变换等,将数据集扩充至2600张。数据标注过程使用Make Sense软件进行,标注格式为YOLO格式,每张图像中的海鲈鱼目标都被精确框选并标注。扩充后的WEEVER数据集同样按照训练集:验证集:测试集=7:2:1的比例进行划分,用于训练和评估改进后的目标检测网络。

功能模块介绍

基于深度学习的海洋牧场鱼类目标检测系统主要包含四个核心功能模块:图像预处理模块、目标检测模块、结果分析模块和用户界面模块。

图像预处理模块

对输入的水下图像进行增强处理,解决水下图像颜色失真和模糊问题。核心算法是基于NSST与PCNN相结合的水下图像融合方法(WNPF)。该方法首先对原始水下图像进行改进的白平衡颜色补偿,然后分别进行Gamma校正和锐化处理,得到两幅不同侧重点的增强图像。接着对这两幅图像进行NSST分解,得到低频子带系数和带通子带系数。然后对低频子带系数采用区域能量自适应加权融合,对带通子带系数利用PCNN神经元点火机制进行融合。最后通过NSST逆变换得到增强后的水下图像。支持批量处理和实时处理两种模式,能够满足不同应用场景的需求。处理后的图像在颜色平衡、对比度和细节保留等方面均有显著改善,为后续的目标检测提供了良好的输入基础。

目标检测模块

目标检测模块是系统的核心,采用改进的CB-YOLOv5s算法进行水下鱼类目标检测。该模块的主要功能包括:

  • 模型加载与初始化:加载预训练的CB-YOLOv5s模型权重,进行必要的初始化设置
  • 目标检测推理:对输入图像进行前向推理,生成目标检测结果,包括边界框坐标、类别和置信度
  • 后处理优化:对检测结果进行非极大值抑制(NMS)等后处理操作,去除冗余检测框
  • 目标计数:统计检测到的目标数量,支持按类别统计
    该模块支持多种输入类型,包括单张图像、图像序列、视频流和摄像头实时输入,具有良好的灵活性和适应性。

结果分析模块

结果分析模块负责对目标检测结果进行分析和可视化展示。主要功能包括:
结果可视化:在原始图像上标注检测到的目标,包括边界框、类别标签和置信度

  • 性能评估:计算检测算法的精度、召回率、mAP等评价指标,评估算法性能
  • 数据统计:统计不同类别目标的数量、分布等信息,生成统计报告
  • 结果导出:支持将检测结果和分析报告导出为图片、视频或文本文件
    该模块提供了丰富的可视化功能,便于用户直观地理解检测结果和分析数据,为海洋牧场管理决策提供支持。

用户界面模块

用户界面模块采用PyQt开发,提供友好的交互界面,方便用户操作和使用系统。主要功能包括:

  • 模型选择:允许用户选择不同的预训练模型权重
  • 输入选择:支持选择图片、视频、文件夹或摄像头作为输入源
  • 参数设置:提供置信度阈值、IoU阈值、帧间延时等参数的调整功能
  • 结果展示:实时显示原始图像和检测结果,包括检测框和计数信息
  • 操作控制:提供开始、暂停、停止等控制按钮,方便用户控制检测过程
    界面设计简洁明了,操作直观便捷,即使是不懂技术的海洋牧场管理人员也能轻松使用。

算法理论

水下图像增强算法

NSST变换原理

非下采样剪切波变换(NSST)是一种多尺度多方向的图像分解方法,具有良好的方向选择性和稀疏表示能力。NSST变换包括两个主要步骤:非下采样金字塔(NSP)分解和非下采样方向滤波器组(NSDFB)分解。NSP分解通过对图像进行迭代式的低通滤波和上采样操作,生成不同尺度的子带图像,避免了传统小波变换中的下采样操作,从而保持了图像的空间分辨率。NSDFB分解则在NSP分解的基础上,对每个尺度的子带图像进行方向滤波,生成多个方向的子带图像。NSST变换的主要优点包括:

  • 具有平移不变性,避免了传统小波变换中的伪吉布斯现象
  • 具有良好的方向选择性,能够有效捕捉图像中的方向特征
  • 具有多尺度分析能力,能够同时处理图像中的细节和全局信息
    在这里插入图片描述

PCNN模型原理

脉冲耦合神经网络(PCNN)是一种基于生物视觉系统的神经网络模型,具有良好的图像分割和特征提取能力。PCNN由多个神经元组成,每个神经元包括接收域、调制域和脉冲产生域三个部分。
接收域负责接收来自相邻神经元的输入信号和外部刺激信号。调制域通过非线性调制将接收域的输出信号转化为内部活动项。脉冲产生域则根据内部活动项和动态阈值生成脉冲输出。
PCNN的主要特点包括:
具有同步脉冲发放特性,能够有效分割图像中的相似区域

  • 具有自适应特性,网络参数能够根据输入信号自动调整
  • 具有鲁棒性,对噪声和光照变化不敏感

WNPF水下图像融合算法

基于NSST与PCNN相结合的水下图像融合方法(WNPF)是本研究提出的一种新型水下图像增强算法。该算法的主要步骤包括:

  • 改进的白平衡颜色补偿:基于灰度世界假设,调整水下图像的颜色平衡,解决颜色失真问题

  • Gamma校正与锐化处理:分别对颜色补偿后的图像进行Gamma校正和锐化处理,得到两幅不同侧重点的增强图像
    在这里插入图片描述

  • NSST分解:对这两幅增强图像进行NSST分解,得到低频子带系数和带通子带系数

  • 低频子带系数融合:采用区域能量自适应加权融合策略,根据区域能量的大小动态调整融合权重

  • 带通子带系数融合:利用PCNN神经元点火机制,选择点火次数较多的神经元输出作为融合结果

  • NSST逆变换:对融合后的子带系数进行NSST逆变换,得到最终的增强图像
    在这里插入图片描述

WNPF算法的创新点在于将NSST的多尺度多方向分解能力与PCNN的自适应特性相结合,同时考虑了水下图像的特点,通过改进的白平衡颜色补偿和多策略融合,有效解决了水下图像颜色失真和模糊问题。

目标检测算法

YOLOvs原理

YOLOv5s是YOLO系列目标检测算法中的轻量级模型,具有检测速度快、精度适中的特点,适合实时应用场景。YOLOv5s网络主要由四个部分组成:输入端、主干网络(Backbone)、特征聚合网络(Neck)和预测网络(Head)。
输入端主要包括Mosaic图像增强、自适应锚框计算和自适应图片缩放等功能。Mosaic图像增强通过整合多个训练图像生成新的训练样本,增加了训练集的多样性和复杂程度。自适应锚框计算根据样本与锚框的匹配度自动选择正负样本,提高检测准确性。自适应缩放则根据目标尺度自适应调整输入图像大小,适应不同尺度目标的检测需求。主干网络采用CSPDarknet结构,主要包括Focus、CSP1_X和SPPF等模块。Focus模块通过对输入特征图进行切片和通道拼接,有效降低计算量并保留重要信息。CSP1_X模块将特征图分为两部分处理,在确保准确性的前提下降低计算复杂度。SPPF模块通过多尺度池化和特征融合,提高模型对不同尺度目标的感知能力。
在这里插入图片描述

特征聚合网络采用FPN和PAN结构,通过自上而下和自下而上的特征融合,将深层特征与浅层特征相结合,同时保留语义信息和位置信息。
预测网络采用Detect模块,通过卷积操作生成目标检测结果,包括边界框坐标、类别和置信度。

CBAM注意力机制

卷积块注意力模块(CBAM)是一种轻量级的注意力机制,由通道注意力模块和空间注意力模块组成。CBAM能够在通道维度和空间维度上自适应地调整特征权重,提高模型对关键特征的关注能力。通道注意力模块通过全局平均池化和全局最大池化获取特征图的通道统计信息,然后通过共享的多层感知机(MLP)生成通道注意力权重。空间注意力模块则通过通道维度的最大池化和平均池化获取特征图的空间统计信息,然后通过卷积操作生成空间注意力权重。
在这里插入图片描述

CBAM的主要优点包括:

  • 参数和计算量小,可以轻松集成到现有网络中
  • 同时考虑通道和空间维度的注意力,提高特征表示能力
  • 具有即插即用特性,可以在网络的不同位置使用

BiFPN特征融合网络

加权双向特征金字塔网络(BiFPN)是一种高效的特征融合网络,在FPN和PANet的基础上进行了改进。BiFPN的主要特点包括:

  • 删除只有一条输入边的节点,减少计算量
  • 在同一层中添加跳跃连接,增强特征流动
  • 采用带权特征融合,根据不同特征的重要性动态调整融合权重
    在这里插入图片描述

BiFPN的带权特征融合机制有三种实现方式:无约束融合、基于Softmax的融合和快速归一化融合。其中,快速归一化融合在保持训练稳定性的同时,具有较高的计算效率,是BiFPN采用的主要方式。

CB-YOLOvs算法

CB-YOLOv5s是本研究提出的改进目标检测算法,通过在YOLOv5s的基础上引入CBAM注意力机制和BiFPN特征融合网络,提高其在水下环境中的检测性能。改进的主要内容包括:

  • 在Backbone的SPP层后添加CBAM注意力机制,增强模型对水下目标特征的提取能力
  • 将Neck部分的FPN和PAN结构替换为BiFPN结构,提高特征融合的效率和效果
  • 调整网络参数和训练策略,适应水下数据集的特点

核心代码介绍

水下图像增强模块

改进的白平衡颜色补偿

import cv
import numpy as np
def improved_white_balance(image):
    # 将图像转换为浮点数格式
    img_float = image.astype(np.float32) / 255.0
    
    # 计算每个通道的平均值
    avg_b = np.mean(img_float[:, :, 0])
    avg_g = np.mean(img_float[:, :, 1])
    avg_r = np.mean(img_float[:, :, 2])
    
    # 计算灰度世界平均值
    avg_gray = (avg_b + avg_g + avg_r) / 3.0
    
    # 计算补偿系数,引入自适应调整因子
    scale_b = avg_gray / avg_b if avg_b != 0 else 1.0
    scale_g = avg_gray / avg_g if avg_g != 0 else 1.0
    scale_r = avg_gray / avg_r if avg_r != 0 else 1.0
    
    # 限制补偿系数的范围,避免过度补偿
    max_scale = 3.0
    min_scale = 0.3
    scale_b = np.clip(scale_b, min_scale, max_scale)
    scale_g = np.clip(scale_g, min_scale, max_scale)
    scale_r = np.clip(scale_r, min_scale, max_scale)
    
    # 应用补偿
    result = np.zeros_like(img_float)
    result[:, :, 0] = img_float[:, :, 0] * scale_b
    result[:, :, 1] = img_float[:, :, 1] * scale_g
    result[:, :, 2] = img_float[:, :, 2] * scale_r
    
    # 裁剪到有效范围
    result = np.clip(result, 0, 1)
    
    # 转换回8位整数格式
    return (result * 255).astype(np.uint8)

该函数实现了改进的白平衡颜色补偿算法,通过计算每个通道的平均值和灰度世界平均值,确定补偿系数,并对补偿系数进行限制,避免过度补偿。该方法能够有效解决水下图像的颜色失真问题,特别是蓝色偏色现象。

NSST分解与融合

import nsst  # 假设使用第三方NSST库
def nsst_decomposition(image, levels, directions):
    """对图像进行NSST分解"""
    # 初始化NSST变换对象
    transform = nsstNSST(image.shape)
    
    # 进行NSST分解
    coeffs = transform.decompose(image, levels=levels, directions=directions)
    
    # 返回低频子带和带通子带
    low_freq = coeffs['low']
    band_pass = coeffs['band_pass']
    
    return low_freq, band_pass
def low_frequency_fusion(low1, low2):
    """低频子带系数融合,采用区域能量自适应加权融合"""
    # 计算区域能量
    energy1 = cv2.boxFilter(low1**2, -1, (3, 3), normalize=False)
    energy2 = cv2.boxFilter(low2**2, -1, (3, 3), normalize=False)
    
    # 计算权重
    total_energy = energy1 + energy2
    weight1 = np.where(total_energy > 0, energy1 / total_energy, 0.5)
    weight2 = 1 - weight1
    
    # 融合
    fused_low = weight1 * low1 + weight2 * low2
    
    return fused_low
def nsst_reconstruction(low_freq, band_pass, levels, directions):
    """NSST逆变换,重建图像"""
    # 初始化NSST变换对象
    transform = nsst.NSST(low_freq.shape)
    
    # 构建系数字典
    coeffs = {
        'low': low_freq,
        'band_pass': band_pass
    }
    
    # 进行NSST逆变换
    reconstructed = transform.reconstruct(coeffs, levels=levels, directions=directions)
    
    return reconstructed

NSST分解、低频子带融合和NSST逆变换功能。其中,低频子带融合采用了区域能量自适应加权融合策略,根据区域能量的大小动态调整融合权重,能够有效保留图像的整体信息和细节特征。

PCNN带通子带融合

class PCNN:
    def __init__(self, shape, alpha=, beta=0.2, theta=10, iteration=5):
        self.shape = shape
        self.alpha = alpha  # 衰减系数
        self.beta = beta    # 连接强度
        self.theta = theta  # 动态阈值
        self.iteration = iteration  # 迭代次数
        
        # 初始化神经元状态
        self.F = np.ones(shape)  # 馈送输入
        self.L = np.zeros(shape)  # 链接输入
        self.U = np.zeros(shape)  # 内部活动项
        self.Y = np.zeros(shape)  # 输出
        self.theta = np.ones(shape) * theta  # 动态阈值
    
    def fit(self, input_image):
        """训练PCNN模型"""
        # 初始化输入
        self.F = input_image.copy()
        
        # 生成链接权重(8邻域)
        kernel = np.array([[0.5, 1, 0.5],
                          [1, 0, 1],
                          [0.5, 1, 0.5]])
        
        # 迭代更新
        pulse_count = np.zeros(self.shape)
        for _ in range(self.iteration):
            # 更新链接输入
            self.L = cv2.filter2D(self.Y, -1, kernel)
            
            # 更新内部活动项
            self.U = self.F * (1 + self.beta * self.L)
            
            # 更新输出
            self.Y = (self.U > self.theta).astype(np.float32)
            
            # 统计脉冲发放次数
            pulse_count += self.Y
            
            # 更新动态阈值
            self.theta = self.theta * np.exp(-self.alpha) + self.theta * self.Y
        
        return pulse_count
def band_pass_fusion(band_pass1, band_pass2):
    """带通子带系数融合,采用PCNN神经元点火机制"""
    fused_band_pass = []
    
    # 对每个尺度和方向的带通子带进行融合
    for scale in range(len(band_pass1)):
        scale_fused = []
        for direction in range(len(band_pass1[scale])):
            # 获取当前子带
            subband1 = band_pass1[scale][direction]
            subband2 = band_pass2[scale][direction]
            
            # 创建PCNN模型
            pcnn = PCNN(subband1.shape)
            
            # 计算每个子带的脉冲发放次数
            pulse_count1 = pcnn.fit(np.abs(subband1))
            pulse_count2 = pcnn.fit(np.abs(subband2))
            
            # 根据脉冲发放次数进行融合
            mask = (pulse_count1 > pulse_count2).astype(np.float32)
            fused_subband = mask * subband1 + (1 - mask) * subband2
            
            scale_fused.append(fused_subband)
        fused_band_pass.append(scale_fused)
    
    return fused_band_pass

PCNN模型和带通子带融合功能。PCNN模型模拟了生物神经元的脉冲发放特性,通过迭代更新神经元状态,生成脉冲输出。带通子带融合则利用PCNN的脉冲发放次数作为决策依据,选择脉冲发放次数较多的子带系数作为融合结果,能够有效保留图像的边缘和纹理信息。

WNPF水下图像增强主函数

def wnpf_enhancement(underwater_image, levels=, directions=[1, 2, 4]):
    """基于NSST与PCNN的水下图像融合增强"""
    # 1. 改进的白平衡颜色补偿
    wb_image = improved_white_balance(underwater_image)
    
    # 2. Gamma校正
    gamma = 0.8
    gamma_image = np.power(wb_image / 255.0, gamma) * 255
    gamma_image = gamma_image.astype(np.uint8)
    
    # 3. 锐化处理
    kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
    sharpened_image = cv2.filter2D(wb_image, -1, kernel)
    sharpened_image = np.clip(sharpened_image, 0, 255).astype(np.uint8)
    
    # 4. 对Gamma校正和锐化后的图像进行NSST分解
    gamma_low, gamma_band_pass = nsst_decomposition(gamma_image, levels, directions)
    sharp_low, sharp_band_pass = nsst_decomposition(sharpened_image, levels, directions)
    
    # 5. 低频子带融合
    fused_low = low_frequency_fusion(gamma_low, sharp_low)
    
    # 6. 带通子带融合
    fused_band_pass = band_pass_fusion(gamma_band_pass, sharp_band_pass)
    
    # 7. NSST逆变换,重建图像
    enhanced_image = nsst_reconstruction(fused_low, fused_band_pass, levels, directions)
    
    # 8. 裁剪到有效范围并转换为8位整数
    enhanced_image = np.clip(enhanced_image, 0, 255).astype(np.uint8)
    
    return enhanced_image

该函数是WNPF水下图像增强算法的主函数,整合了改进的白平衡颜色补偿、Gamma校正、锐化处理、NSST分解、子带融合和NSST逆变换等步骤。通过该函数,可以将输入的水下图像转换为增强后的清晰图像,有效解决水下图像颜色失真和模糊问题。

目标检测模块

CBAM注意力机制实现

import torch
import torchnn as nn
import torch.nn.functional as F
class ChannelAttention(nn.Module):
    def __init__(self, in_channels, reduction_ratio=16):
        super(ChannelAttention, self).__init__()
        # 全局平均池化和全局最大池化
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.max_pool = nn.AdaptiveMaxPool2d(1)
        
        # 共享MLP
        self.mlp = nn.Sequential(
            nn.Conv2d(in_channels, in_channels // reduction_ratio, kernel_size=1, bias=False),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels // reduction_ratio, in_channels, kernel_size=1, bias=False)
        )
        
        # Sigmoid激活函数
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        # 计算平均池化和最大池化特征
        avg_out = self.mlp(self.avg_pool(x))
        max_out = self.mlp(self.max_pool(x))
        
        # 特征相加并经过Sigmoid激活
        out = avg_out + max_out
        out = self.sigmoid(out)
        
        # 与输入特征相乘
        return x * out
class SpatialAttention(nn.Module):
    def __init__(self, kernel_size=7):
        super(SpatialAttention, self).__init__()
        # 卷积层,用于生成空间注意力图
        self.conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size//2, bias=False)
        # Sigmoid激活函数
        self.sigmoid = nn.Sigmoid()
    
    def forward(self, x):
        # 计算通道维度的最大池化和平均池化
        max_out, _ = torch.max(x, dim=1, keepdim=True)
        avg_out = torch.mean(x, dim=1, keepdim=True)
        
        # 特征拼接
        out = torch.cat([max_out, avg_out], dim=1)
        
        # 经过卷积和Sigmoid激活
        out = self.conv(out)
        out = self.sigmoid(out)
        
        # 与输入特征相乘
        return x * out
class CBAM(nn.Module):
    def __init__(self, in_channels, reduction_ratio=16, kernel_size=7):
        super(CBAM, self).__init__()
        # 通道注意力模块
        self.channel_attention = ChannelAttention(in_channels, reduction_ratio)
        # 空间注意力模块
        self.spatial_attention = SpatialAttention(kernel_size)
    
    def forward(self, x):
        # 先经过通道注意力模块,再经过空间注意力模块
        x = self.channel_attention(x)
        x = self.spatial_attention(x)
        return x

CBAM注意力机制,包括通道注意力模块和空间注意力模块。通道注意力模块通过全局平均池化和全局最大池化获取通道特征,然后通过共享MLP生成通道注意力权重。空间注意力模块通过通道维度的最大池化和平均池化获取空间特征,然后通过卷积生成空间注意力权重。最后,将两个模块串联,实现通道和空间维度的双重注意力。

BiFPN特征融合网络实现

class BiFPNLayer(nnModule):
    def __init__(self, in_channels, out_channels):
        super(BiFPNLayer, self).__init__()
        # 卷积层,用于调整通道数
        self.conv_layers = nn.ModuleList([
            nn.Conv2d(in_c, out_channels, kernel_size=1, bias=False) 
            for in_c in in_channels
        ])
        
        # 卷积层,用于特征融合后的处理
        self.fusion_convs = nn.ModuleList([
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False) 
            for _ in range(len(in_channels))
        ])
        
        # 批量归一化
        self.bns = nn.ModuleList([
            nn.BatchNorm2d(out_channels) 
            for _ in range(len(in_channels))
        ])
        
        # 激活函数
        self.relu = nn.ReLU(inplace=True)
        
        # 可学习的权重参数
        self.weights = nn.ParameterList([
            nn.Parameter(torch.ones(len(in_channels), requires_grad=True)) 
            for _ in range(len(in_channels))
        ])
        
        # 防止除以零的epsilon
        self.epsilon = 1e-4
    
    def forward(self, inputs):
        # 调整输入特征的通道数
        features = [conv(x) for conv, x in zip(self.conv_layers, inputs)]
        
        # 特征融合
        outputs = []
        for i in range(len(features)):
            # 计算当前层级的融合权重
            w = self.weights[i]
            w = F.relu(w) / (torch.sum(w, dim=0) + self.epsilon)
            
            # 根据不同层级的特点进行特征融合
            # 这里简化处理,实际实现需要根据BiFPN的结构进行调整
            fused_feature = sum(w[j] * features[j] for j in range(len(features)))
            
            # 融合后处理
            fused_feature = self.fusion_convs[i](fused_feature)
            fused_feature = self.bns[i](fused_feature)
            fused_feature = self.relu(fused_feature)
            
            outputs.append(fused_feature)
        
        return outputs
class BiFPN(nn.Module):
    def __init__(self, in_channels_list, out_channels, num_layers=3):
        super(BiFPN, self).__init__()
        # BiFPN层数
        self.num_layers = num_layers
        
        # BiFPN层列表
        self.bifpn_layers = nn.ModuleList([
            BiFPNLayer(in_channels_list, out_channels)
            for _ in range(num_layers)
        ])
    
    def forward(self, inputs):
        features = inputs
        
        # 逐层进行特征融合
        for layer in self.bifpn_layers:
            features = layer(features)
        
        return features

BiFPN特征融合网络,包括BiFPNLayer和BiFPN两个类。BiFPNLayer实现了单个BiFPN层的特征融合功能,通过可学习的权重参数,动态调整不同输入特征的融合权重。BiFPN类则通过堆叠多个BiFPNLayer,实现多层特征融合,提高特征表示能力。

CB-YOLOvs模型构建

import torch
from models.common import Conv, Bottleneck, C3, SPPF
from models.experimental import Ensemble
from utils.autoanchor import check_anchor_order
from utils.general import LOGGER, check_version, make_divisible
from utils.plots import feature_visualization
from utils.torch_utils import (fuse_conv_and_bn, initialize_weights, model_info, 
                              scale_img, select_device, time_sync)
class CB_YOLOv5s(nn.Module):
    def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None):
        super(CB_YOLOv5s, self).__init__()
        # 加载配置
        if isinstance(cfg, dict):
            self.yaml = cfg  # model dict
        else:  # is *.yaml
            import yaml  # for torch hub
            self.yaml_file = Path(cfg).name
            with open(cfg, 'r') as f:
                self.yaml = yaml.safe_load(f)
        
        # 设置类别数量
        if nc and nc != self.yaml['nc']:
            LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
            self.yaml['nc'] = nc  # override yaml value
        
        # 设置锚框
        if anchors:
            LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}')
            self.yaml['anchors'] = round(anchors)  # override yaml value
        
        # 构建模型
        self.model, self.save = parse_model(self.yaml, ch=[ch])  # model, savelist
        
        # 初始化权重
        initialize_weights(self)
        
        # 检查锚框顺序
        m = self.model[-1]  # Detect()
        if isinstance(m, Detect):
            s = 256  # 2x min stride
            m.inplace = self.yaml.get('inplace', True)
            m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))])  # forward
            check_anchor_order(m)
            m.anchors /= m.stride.view(-1, 1, 1)
            self.stride = m.stride
            self._initialize_biases()  # only run once
    
    def forward(self, x):
        # 前向传播
        return self.forward_once(x)
    
    def forward_once(self, x):
        # 保存中间特征用于特征融合
        y, dt = [], []  # outputs
        for m in self.model:
            if m.f != -1:  # if not from previous layer
                x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f]
            
            # 前向传播
            x = m(x)
            y.append(x if m.i in self.save else None)  # save output
        
        return x
def parse_model(d, ch):
    # 解析模型配置,构建网络结构
    LOGGER.info(f"{'':3}{'from':>18}{'n':>3}{'params':>10}  {'module':<40}{'arguments':<30}")
    anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple']
    na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors  # number of anchors
    no = na * (nc + 5)  # number of outputs = anchors * (classes + 5)
    
    layers, save, c2 = [], [], ch[-1]
    for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):
        m = eval(m) if isinstance(m, str) else m
        for j, a in enumerate(args):
            try:
                args[j] = eval(a) if isinstance(a, str) else a
            except NameError:
                pass
        
        n = max(round(n * gd), 1) if n > 1 else n
        if m in [Conv, Bottleneck, C3, SPPF, CBAM, BiFPN]:
            c1, c2 = ch[f], args[0]
            if c2 != no:
                c2 = make_divisible(c2 * gw, 8)
            
            args = [c1, c2] + args[1:]
            if m == C3:
                args.insert(2, n)  # number of Bottlenecks
                n = 1
        
        # 特别处理CBAM和BiFPN
        if m == CBAM:
            args = [c2]  # CBAM只需要输入通道数
        
        # 构建模块
        m_ = nn.Sequential(*[m(*args) for _ in range(n)]) if n > 1 else m(*args)
        t = str(m)[8:-2].replace('__main__.', '')
        LOGGER.info(f'{i:3}{str(f):>18}{n:>3}{sum(p.numel() for p in m_.parameters()):>10.0f}  {t:<40}{str(args):<30}')
        
        # 特殊处理SPPF后添加CBAM
        if t == 'SPPF':
            cbam = CBAM(c2)
            m_ = nn.Sequential(m_, cbam)
        
        # 特殊处理FPN和PAN,替换为BiFPN
        if t == 'Concat' and i > 0:  # 假设在head部分的Concat替换为BiFPN
            # 获取输入特征的通道数
            in_channels = [ch[j] for j in f]
            bifpn = BiFPN(in_channels, c2)
            m_ = bifpn
        
        save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)
        layers.append(m_)
        ch.append(c2)
    
    return nn.Sequential(*layers), sorted(save)

CB-YOLOv5s模型的构建,通过修改YOLOv5s的网络结构,在Backbone的SPP层后添加CBAM注意力机制,并将Head部分的FPN和PAN结构替换为BiFPN结构。parse_model函数负责解析模型配置并构建网络层,特别处理了CBAM和BiFPN的添加逻辑。

目标检测推理函数

def detect(image, model, conf_thres=, iou_thres=0.45):
    """目标检测推理"""
    # 图像预处理
    img = letterbox(image, new_shape=640)[0]  # 调整图像大小
    img = img[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
    img = np.ascontiguousarray(img)
    
    # 转换为Tensor
    img = torch.from_numpy(img).to(device)
    img = img.half() if model.fp16 else img.float()  # uint8 to fp16/32
    img /= 255.0  # 0 - 255 to 0.0 - 1.0
    if img.ndimension() == 3:
        img = img.unsqueeze(0)
    
    # 推理
    pred = model(img)[0]
    
    # 应用非极大值抑制
    pred = non_max_suppression(pred, conf_thres, iou_thres)
    
    # 后处理
    results = []
    for i, det in enumerate(pred):
        if len(det):
            # 调整坐标到原始图像尺寸
            det[:, :4] = scale_coords(img.shape[2:], det[:, :4], image.shape).round()
            
            # 提取结果
            for *xyxy, conf, cls in reversed(det):
                results.append({
                    'bbox': [int(x) for x in xyxy],
                    'confidence': float(conf),
                    'class': int(cls)
                })
    
    return results
def count_objects(results):
    """统计检测到的目标数量"""
    count = {}
    for obj in results:
        cls = obj['class']
        if cls not in count:
            count[cls] = 0
        count[cls] += 1
    return count

目标检测的推理和结果处理功能。detect函数负责对输入图像进行预处理、模型推理和后处理,输出检测结果。count_objects函数则统计检测到的不同类别目标的数量,为用户提供直观的数据统计。

用户界面模块

主界面类实现

import sys
from PyQtQtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                            QHBoxLayout, QPushButton, QLabel, QFileDialog, 
                            QComboBox, QSlider, QSpinBox, QDoubleSpinBox, 
                            QGroupBox, QTabWidget, QTreeView, QLineEdit)
from PyQt5.QtGui import QPixmap, QImage, QFont
from PyQt5.QtCore import Qt, QTimer, QDir, QModelIndex
import cv2
import numpy as np
class FishDetectionSystem(QMainWindow):
    def __init__(self):
        super().__init__()
        # 初始化变量
        self.model = None
        self.current_image = None
        self.detection_results = []
        self.camera = None
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_frame)
        
        # 设置窗口标题和大小
        self.setWindowTitle('海洋鱼类检测与计数系统')
        self.setGeometry(100, 100, 1200, 800)
        
        # 初始化界面
        self.init_ui()
    
    def init_ui(self):
        # 创建主部件和布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QHBoxLayout(central_widget)
        
        # 左侧控制面板
        control_panel = QWidget()
        control_layout = QVBoxLayout(control_panel)
        control_panel.setFixedWidth(300)
        
        # 模型选择部分
        model_group = QGroupBox('模型选择')
        model_layout = QVBoxLayout(model_group)
        
        self.model_combo = QComboBox()
        self.model_combo.addItems(['CBAM-BiFPN-yolov5s.pt', 'yolov5s.pt'])
        model_layout.addWidget(QLabel('选择预训练模型:'))
        model_layout.addWidget(self.model_combo)
        
        load_model_btn = QPushButton('加载模型')
        load_model_btn.clicked.connect(self.load_model)
        model_layout.addWidget(load_model_btn)
        
        self.model_info_label = QLabel('未加载模型')
        model_layout.addWidget(self.model_info_label)
        
        # 输入选择部分
        input_group = QGroupBox('输入选择')
        input_layout = QVBoxLayout(input_group)
        
        self.input_type_combo = QComboBox()
        self.input_type_combo.addItems(['图片', '视频', '文件夹', '摄像头'])
        input_layout.addWidget(QLabel('输入类型:'))
        input_layout.addWidget(self.input_type_combo)
        
        select_file_btn = QPushButton('选择文件')
        select_file_btn.clicked.connect(self.select_input)
        input_layout.addWidget(select_file_btn)
        
        self.file_path_label = QLabel('未选择文件')
        input_layout.addWidget(self.file_path_label)
        
        # 参数设置部分
        param_group = QGroupBox('参数设置')
        param_layout = QVBoxLayout(param_group)
        
        # 置信度阈值
        conf_layout = QHBoxLayout()
        conf_layout.addWidget(QLabel('置信度阈值:'))
        self.conf_slider = QSlider(Qt.Horizontal)
        self.conf_slider.setRange(1, 100)
        self.conf_slider.setValue(25)
        self.conf_slider.valueChanged.connect(self.update_conf_value)
        conf_layout.addWidget(self.conf_slider)
        self.conf_value = QLabel('0.25')
        conf_layout.addWidget(self.conf_value)
        param_layout.addLayout(conf_layout)
        
        # IoU阈值
        iou_layout = QHBoxLayout()
        iou_layout.addWidget(QLabel('IoU阈值:'))
        self.iou_slider = QSlider(Qt.Horizontal)
        self.iou_slider.setRange(1, 100)
        self.iou_slider.setValue(45)
        self.iou_slider.valueChanged.connect(self.update_iou_value)
        iou_layout.addWidget(self.iou_slider)
        self.iou_value = QLabel('0.45')
        iou_layout.addWidget(self.iou_value)
        param_layout.addLayout(iou_layout)
        
        # 帧间延时
        latency_layout = QHBoxLayout()
        latency_layout.addWidget(QLabel('帧间延时(ms):'))
        self.latency_spin = QSpinBox()
        self.latency_spin.setRange(1, 1000)
        self.latency_spin.setValue(30)
        latency_layout.addWidget(self.latency_spin)
        param_layout.addLayout(latency_layout)
        
        # 控制按钮
        control_btns_layout = QHBoxLayout()
        self.start_btn = QPushButton('开始')
        self.start_btn.clicked.connect(self.start_detection)
        control_btns_layout.addWidget(self.start_btn)
        
        self.pause_btn = QPushButton('暂停')
        self.pause_btn.clicked.connect(self.pause_detection)
        self.pause_btn.setEnabled(False)
        control_btns_layout.addWidget(self.pause_btn)
        
        self.stop_btn = QPushButton('停止')
        self.stop_btn.clicked.connect(self.stop_detection)
        self.stop_btn.setEnabled(False)
        control_btns_layout.addWidget(self.stop_btn)
        
        # 计数结果显示
        count_group = QGroupBox('计数结果')
        count_layout = QVBoxLayout(count_group)
        self.count_label = QLabel('未检测')
        count_layout.addWidget(self.count_label)
        
        # 添加所有组到控制面板
        control_layout.addWidget(model_group)
        control_layout.addWidget(input_group)
        control_layout.addWidget(param_group)
        control_layout.addLayout(control_btns_layout)
        control_layout.addWidget(count_group)
        control_layout.addStretch()
        
        # 右侧显示区域
        display_panel = QWidget()
        display_layout = QVBoxLayout(display_panel)
        
        # 标签页用于显示原始图像和检测结果
        self.tab_widget = QTabWidget()
        
        # 原始图像标签页
        self.original_image_label = QLabel()
        self.original_image_label.setAlignment(Qt.AlignCenter)
        self.original_image_label.setText('原始图像')
        self.tab_widget.addTab(self.original_image_label, '原始图像')
        
        # 检测结果标签页
        self.result_image_label = QLabel()
        self.result_image_label.setAlignment(Qt.AlignCenter)
        self.result_image_label.setText('检测结果')
        self.tab_widget.addTab(self.result_image_label, '检测结果')
        
        display_layout.addWidget(self.tab_widget)
        
        # 添加到主布局
        main_layout.addWidget(control_panel)
        main_layout.addWidget(display_panel, 1)
    
    def load_model(self):
        # 加载模型的实现
        model_path = self.model_combo.currentText()
        # 这里应该调用模型加载函数
        self.model_info_label.setText(f'已加载模型: {model_path}')
    
    def select_input(self):
        # 选择输入的实现
        input_type = self.input_type_combo.currentText()
        if input_type == '图片':
            file_path, _ = QFileDialog.getOpenFileName(self, '选择图片', '', 'Image Files (*.png *.jpg *.bmp)')
        elif input_type == '视频':
            file_path, _ = QFileDialog.getOpenFileName(self, '选择视频', '', 'Video Files (*.mp4 *.avi *.mov)')
        elif input_type == '文件夹':
            file_path = QFileDialog.getExistingDirectory(self, '选择文件夹', '')
        else:  # 摄像头
            file_path = 'camera'
        
        self.file_path_label.setText(file_path)
    
    def update_conf_value(self):
        # 更新置信度值显示
        value = self.conf_slider.value() / 100
        self.conf_value.setText(f'{value:.2f}')
    
    def update_iou_value(self):
        # 更新IoU值显示
        value = self.iou_slider.value() / 100
        self.iou_value.setText(f'{value:.2f}')
    
    def start_detection(self):
        # 开始检测的实现
        input_type = self.input_type_combo.currentText()
        if input_type == '摄像头':
            self.camera = cv2.VideoCapture(0)
            self.timer.start(self.latency_spin.value())
        else:
            # 处理图片或视频
            file_path = self.file_path_label.text()
            if file_path and file_path != '未选择文件':
                if input_type == '图片':
                    self.process_image(file_path)
                else:  # 视频或文件夹
                    # 实现视频或文件夹处理
                    pass
        
        # 更新按钮状态
        self.start_btn.setEnabled(False)
        self.pause_btn.setEnabled(True)
        self.stop_btn.setEnabled(True)
    
    def pause_detection(self):
        # 暂停检测的实现
        if self.timer.isActive():
            self.timer.stop()
            self.pause_btn.setText('继续')
        else:
            self.timer.start(self.latency_spin.value())
            self.pause_btn.setText('暂停')
    
    def stop_detection(self):
        # 停止检测的实现
        self.timer.stop()
        if self.camera is not None:
            self.camera.release()
            self.camera = None
        
        # 更新按钮状态
        self.start_btn.setEnabled(True)
        self.pause_btn.setEnabled(False)
        self.pause_btn.setText('暂停')
        self.stop_btn.setEnabled(False)
    
    def update_frame(self):
        # 更新摄像头帧
        if self.camera is not None and self.camera.isOpened():
            ret, frame = self.camera.read()
            if ret:
                # 处理帧并显示
                self.process_frame(frame)
    
    def process_image(self, image_path):
        # 处理单张图片
        image = cv2.imread(image_path)
        self.current_image = image
        self.display_image(image, self.original_image_label)
        
        # 进行目标检测
        if self.model is not None:
            conf_thres = self.conf_slider.value() / 100
            iou_thres = self.iou_slider.value() / 100
            results = detect(image, self.model, conf_thres, iou_thres)
            self.detection_results = results
            
            # 绘制检测结果
            result_image = self.draw_results(image, results)
            self.display_image(result_image, self.result_image_label)
            
            # 更新计数结果
            count = count_objects(results)
            self.update_count_display(count)
    
    def process_frame(self, frame):
        # 处理视频帧
        self.display_image(frame, self.original_image_label)
        
        # 进行目标检测
        if self.model is not None:
            conf_thres = self.conf_slider.value() / 100
            iou_thres = self.iou_slider.value() / 100
            results = detect(frame, self.model, conf_thres, iou_thres)
            
            # 绘制检测结果
            result_image = self.draw_results(frame, results)
            self.display_image(result_image, self.result_image_label)
            
            # 更新计数结果
            count = count_objects(results)
            self.update_count_display(count)
    
    def display_image(self, image, label):
        # 在标签上显示图像
        rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
        h, w, ch = rgb_image.shape
        bytes_per_line = ch * w
        q_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
        
        # 调整图像大小以适应标签
        pixmap = QPixmap.fromImage(q_image)
        scaled_pixmap = pixmap.scaled(label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
        
        label.setPixmap(scaled_pixmap)
    
    def draw_results(self, image, results):
        # 在图像上绘制检测结果
        result_image = image.copy()
        for obj in results:
            bbox = obj['bbox']
            confidence = obj['confidence']
            cls = obj['class']
            
            # 绘制边界框
            cv2.rectangle(result_image, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 0, 255), 2)
            
            # 绘制类别和置信度
            label = f'鱼类: {confidence:.2f}'
            cv2.putText(result_image, label, (bbox[0], bbox[1] - 10), 
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
        
        return result_image
    
    def update_count_display(self, count):
        # 更新计数结果显示
        if not count:
            self.count_label.setText('未检测到目标')
        else:
            count_text = '检测到的目标:\n'
            for cls, num in count.items():
                count_text += f'鱼类: {num}\n'
            self.count_label.setText(count_text)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = FishDetectionSystem()
    window.show()
    sys.exit(app.exec_())

海洋鱼类检测与计数系统的用户界面,采用PyQt5开发,提供了友好的交互界面。主要功能包括模型选择、输入选择、参数设置、检测控制和结果显示等。界面设计简洁明了,操作直观便捷,方便用户使用。

重难点和创新点

主要难点

水下图像增强难题:水下环境复杂,光线衰减严重,导致水下图像普遍存在颜色失真、对比度低和细节模糊等问题。传统的图像增强算法难以同时解决这些问题,需要开发新的增强方法。

  • 目标检测的适应性问题:水下鱼类目标形态多样,活动频繁,常常出现相互遮挡和与背景颜色相似的情况,增加了目标检测的难度。现有的目标检测算法在水下环境中的表现往往不尽如人意。
  • 数据集构建挑战:高质量的水下鱼类数据集获取困难,需要大量的人力和物力投入。同时,水下图像数据存在类别不平衡、标注困难等问题,增加了数据集构建的难度。
  • 实时性与准确性的平衡:海洋牧场鱼类检测需要在保证检测准确性的同时,满足实时性要求,以便及时反馈鱼类活动状态。如何在两者之间取得平衡是一个重要挑战。

主要创新点

提出WNPF水下图像融合增强算法:结合NSST的多尺度多方向分解能力和PCNN的自适应特性,通过改进的白平衡颜色补偿、多策略融合等方法,有效解决了水下图像颜色失真和模糊问题。该算法在信息熵、UCIQE和UIQM等客观评价指标上均优于传统的水下图像增强算法。

  • 提出CB-YOLOv5s目标检测算法:通过在YOLOv5s的Backbone部分添加CBAM注意力机制,并将特征金字塔结构替换为BiFPN结构,显著提升了检测准确率。CBAM注意力机制从通道和空间两个维度增强模型对水下目标的关注,BiFPN特征融合网络则提高了不同尺度特征的融合效果。
  • 构建WEEVER海鲈鱼数据集:从青岛鲁海丰海洋牧场采集海鲈鱼图像,并通过数据扩充方法将数据集从642张扩充至2600张,为水下鱼类目标检测研究提供了重要的数据支持。该数据集包含了真实海洋牧场环境中的海鲈鱼图像,具有较高的应用价值。
  • 设计实现海洋鱼类检测与计数系统:基于PyQt5开发了一套功能完善、操作便捷的海洋鱼类检测与计数系统,支持多种输入类型和参数调整,为海洋牧场管理人员提供了实用的工具。该系统实现了检测结果的可视化展示和目标计数功能,便于用户直观地了解检测结果。
  • 综合优化策略:将水下图像增强与目标检测相结合,通过对增强后的图像进行目标检测,进一步提高了检测性能。

总结

本研究针对海洋牧场鱼类目标检测过程中遇到的水下图像颜色失真、模糊以及目标相互遮挡、同背景色难以区分等问题,提出了一系列改进方法,并设计实现了海洋鱼类检测与计数系统。主要研究成果包括:
数据集构建:构建了URPC2021和WEEVER两个水下鱼类数据集,并对其进行了标注和扩充,为后续研究提供了数据基础。

  • 水下图像增强算法:提出了基于NSST与PCNN相结合的WNPF水下图像融合增强算法,有效解决了水下图像颜色失真和模糊问题。
  • 目标检测算法改进:提出了CB-YOLOv5s目标检测算法,通过添加CBAM注意力机制和BiFPN特征融合网络,显著提升了检测准确率。在URPC2021数据集上的准确率达到86.6%,在WEEVER数据集上达到89.7%,分别比原YOLOv5s网络提高5.3%和16.7%。
  • 系统实现:设计实现了海洋鱼类检测与计数系统,支持多种输入类型和参数调整,提供了友好的用户界面和丰富的功能,为海洋牧场管理人员提供了实用的工具。
  • 综合应用:将水下图像增强与目标检测相结合,通过对增强后的图像进行目标检测,进一步提高了检测性能,验证了方法的有效性和实用性。
    本研究的成果为海洋牧场鱼类目标检测提供了新的思路和方法,有助于提高海洋牧场的智能化管理水平,促进海洋渔业的可持续发展。未来的研究可以进一步优化算法性能,提高处理速度,拓展系统功能,使其更好地适应实际应用需求。
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐