YOLOv8改进 | ICLR 2022 |ODConv附修改后的C2f、Bottleneck模块代码

论文地址:论文地址点击即可跳转阅读

代码地址:文末提供复制粘贴的代码块

一、本文介绍

这篇文章给大家带来的是发表于2022ICLRODConv(Omni-Dimensional Dynamic Convolution)中文名字全维度动态卷积,该卷积可以即插即用,可以直接替换网络结构中的任何一个卷积模块,在本文的末尾提供可以直接替换卷积模块的ODConv,添加ODConv模块的C2f和Bottleneck(配合教程将代码复制粘贴到你自己的代码中即可运行)给大家该卷积模块主要具有更小的计算量和更高的精度,其中添加ODConv模块的网络(只替换了一处C2f中的卷积)参数量由8.9GFLOPS减小到8.8GFLOPS,精度也有提高->下面的图片是精度的对比(因为训练成本我只是用了相同的数据集100张图片除了修改了ODConv以后其他配置都相同下面是效果对比图左面为修改版本,右面为基础版本)

适用场景:轻量化、所有检测目标时均可使用进行有效涨点

二、基本原理介绍

大家估计只是冲着代码来看,估计很少想要看其原理的,所以我们这里只做略微的介绍和简单的讲解,最起码知道其基本的原理。

2.1ODConv基本原理介绍 

ODConv的创新之处在于它采用了一种多维注意力机制。这种机制通过并行策略来学习卷积核在核空间所有四个维度(即空间大小、输入通道数和每个卷积层的输出通道数)上的互补注意力。这种方法适用于任何卷积层,增强了网络的灵活性和适应性(这个四个维度的卷积可以在代码中清晰的体现出来)

ODConv卷积主要的改进机制就是在上面的地方体现出来的,这个变量名aggregate_weight就是文中提到的四个维度的卷积核,其中的通道数[16,16,3,3]的含义为其具有16个卷积核,每个卷积核有16个通道,卷积核的大小是3x3,所以这处就是体现其具体改动的地方,这处的代码可以在我文末提供的代码中可以找到如果想要了解可以自己debug看一下。

那么大家可能想问了,它是如何减少计算量的呢?因为他具有16个卷积核16个通道数所以他计算是通过并行的方式,因为这一机制导致它的计算量也变小了(是不是感觉一举两得)。

总结:其实ODConv就是提出了这么一种具有多维度的卷积核所以其减少了计算量同时提高了检测精度。

上面的总结是我个人的理解,下面的是文章中的总结-> 

2.2论文总结

Omni-Dimensional Dynamic Convolution(ODConv)的基本原理是对传统的卷积神经网络(CNN)中卷积层的设计进行创新。在传统的CNN中,每个卷积层通常使用固定的、静态的卷积核来提取特征。相比之下,ODConv引入了一种动态的、多维的注意力机制,对卷积核的设计进行了全面的改进。下面详细介绍其原理:

  1. 多维动态注意力机制:ODConv的核心创新是其多维动态注意力机制。传统的动态卷积通常只在卷积核数量这一个维度上实现动态性,即通过对多个卷积核进行加权组合以适应不同的输入特征。ODConv则进一步扩展了这一概念,它不仅在卷积核数量上动态调整,还涉及到卷积核的其他三个维度:空间大小、输入通道数、输出通道数。这意味着ODConv能够更精细地适应输入数据的特征,从而提高特征提取的效果。

  2. 并行策略:ODConv采用并行策略来同时学习不同维度上的注意力。这种策略允许网络在处理每个维度的特征时更加高效,同时确保各维度之间的互补性和协同作用。

下面的图片左边的是传统的动态卷积右边是文章中提出的ODConv。 

ODConv的独特之处:与DyConv和CondConv不同,ODConv采用了一种新颖的多维注意力机制。这种机制不是仅针对每个卷积核计算一个单一的注意力标量,而是沿着卷积核空间的所有四个维度计算四种类型的注意力:αsi​, αci​, αfi​, 和αwi​。这样的设计允许ODConv在空间大小、输入通道数、过滤器数量(输出通道数)和卷积核数量这四个维度上进行细粒度的动态调整。这一处在代码中也有清晰的体现如下图->

并行计算:ODConv能够以并行的方式计算这四种类型的注意力,这提高了其效率,并允许它更全面地捕捉和利用输入数据的多维特征。

上面提到了每个通道分别计算其注意力机制,那么其是如何生效的呢?并且提高模型精度的呢? 

下面图片中分别具有a,b,c,d代表四个不同通道的注意力机制生效方式。 

如何在其四种不同类型的注意力机制中逐步应用到卷积核上的过程的呢,下面是对这一过程的解释:

  1. 位置维度的逐点乘法(Location-wise Multiplication):如上图(a)所示,这一步涉及到沿着卷积核的空间维度(即卷积核的高度和宽度)进行的乘法操作。在这里,ODConv计算的位置维度注意力(αsi​)被应用于卷积核的每个空间位置上,这允许网络动态调整卷积核在处理不同空间位置的信息时的重要性。

  2. 通道维度的逐通道乘法(Channel-wise Multiplication):如上图(b)所示,这一步是沿着输入通道维度进行的乘法操作。在这个阶段,通道维度的注意力(αci​)被应用于卷积核的每个输入通道上,使得网络能够针对不同的输入特征通道动态调整其处理方式。

  3. 输出通道维度的逐滤波器乘法(Filter-wise Multiplication):如上图(c)所示,这一步是沿着输出通道维度进行的乘法操作。在这里,输出通道维度的注意力(αfi​)影响卷积核的每个输出滤波器,从而使网络能够根据不同输出特征的重要性进行调整。

  4. 卷积核维度的逐核乘法(Kernel-wise Multiplication):如上图(d)所示,这一步是沿着卷积核维度进行的乘法操作。卷积核维度的注意力(αwi​)在这一阶段被应用,它允许网络动态调整不同卷积核的重要性。

通过这种分步骤的乘法操作,ODConv能够在卷积过程中综合考虑空间位置、输入通道、输出通道和卷积核数量等多个维度的信息,从而实现对卷积核的全方位动态调整。这种细致的调整方式使得ODConv能够更精准地提取特征,增强网络的学习和表达能力。论文的方法部分对这些操作的具体数学表达和实现细节(大家有兴趣可以看看论文,论文在开头我提供了地址)。

三、ODConv代码 

下面的代码就是ODConv的代码,大家可以创建一个同样的文件如下"ultralytics/nn/modules/ODConv.py" ,我们创建该文件之后将下面的代码复制粘贴到其中。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.autograd


class Attention(nn.Module):
    def __init__(self, in_planes, out_planes, kernel_size, groups=1, reduction=0.0625, kernel_num=4, min_channel=16):
        super(Attention, self).__init__()
        attention_channel = max(int(in_planes * reduction), min_channel)
        self.kernel_size = kernel_size
        self.kernel_num = kernel_num
        self.temperature = 1.0

        self.avgpool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Conv2d(in_planes, attention_channel, 1, bias=False)
        self.bn = nn.BatchNorm2d(attention_channel)
        self.relu = nn.ReLU(inplace=True)

        self.channel_fc = nn.Conv2d(attention_channel, in_planes, 1, bias=True)
        self.func_channel = self.get_channel_attention

        if in_planes == groups and in_planes == out_planes:  # depth-wise convolution
            self.func_filter = self.skip
        else:
            self.filter_fc = nn.Conv2d(attention_channel, out_planes, 1, bias=True)
            self.func_filter = self.get_filter_attention

        if kernel_size == 1:  # point-wise convolution
            self.func_spatial = self.skip
        else:
            self.spatial_fc = nn.Conv2d(attention_channel, kernel_size * kernel_size, 1, bias=True)
            self.func_spatial = self.get_spatial_attention

        if kernel_num == 1:
            self.func_kernel = self.skip
        else:
            self.kernel_fc = nn.Conv2d(attention_channel, kernel_num, 1, bias=True)
            self.func_kernel = self.get_kernel_attention

        self._initialize_weights()

    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            if isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def update_temperature(self, temperature):
        self.temperature = temperature

    @staticmethod
    def skip(_):
        return 1.0

    def get_channel_attention(self, x):
        channel_attention = torch.sigmoid(self.channel_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
        return channel_attention

    def get_filter_attention(self, x):
        filter_attention = torch.sigmoid(self.filter_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
        return filter_attention

    def get_spatial_attention(self, x):
        spatial_attention = self.spatial_fc(x).view(x.size(0), 1, 1, 1, self.kernel_size, self.kernel_size)
        spatial_attention = torch.sigmoid(spatial_attention / self.temperature)
        return spatial_attention

    def get_kernel_attention(self, x):
        kernel_attention = self.kernel_fc(x).view(x.size(0), -1, 1, 1, 1, 1)
        kernel_attention = F.softmax(kernel_attention / self.temperature, dim=1)
        return kernel_attention

    def forward(self, x):
        x = self.avgpool(x)
        x = self.fc(x)
        # x = self.bn(x) # 在外面我提供了一个bn这里会报错
        x = self.relu(x)
        return self.func_channel(x), self.func_filter(x), self.func_spatial(x), self.func_kernel(x)


class ODConv2d(nn.Module):
    def __init__(self, in_planes, out_planes, kernel_size, stride=1, padding=1, dilation=1, groups=1,
                 reduction=0.0625, kernel_num=4):
        super(ODConv2d, self).__init__()
        kernel_size = kernel_size[0]
        in_planes = in_planes
        self.in_planes = in_planes
        self.out_planes = out_planes
        self.kernel_size = kernel_size
        self.stride = stride
        self.padding = padding
        self.dilation = dilation
        self.groups = groups
        self.kernel_num = kernel_num
        self.attention = Attention(in_planes, out_planes, kernel_size, groups=groups,
                                   reduction=reduction, kernel_num=kernel_num)
        self.weight = nn.Parameter(torch.randn(kernel_num, out_planes, in_planes//groups, kernel_size, kernel_size),
                                   requires_grad=True)
        self._initialize_weights()

        if self.kernel_size == 1 and self.kernel_num == 1:
            self._forward_impl = self._forward_impl_pw1x
        else:
            self._forward_impl = self._forward_impl_common

    def _initialize_weights(self):
        for i in range(self.kernel_num):
            nn.init.kaiming_normal_(self.weight[i], mode='fan_out', nonlinearity='relu')

    def update_temperature(self, temperature):
        self.attention.update_temperature(temperature)

    def _forward_impl_common(self, x):
        # Multiplying channel attention (or filter attention) to weights and feature maps are equivalent,
        # while we observe that when using the latter method the models will run faster with less gpu memory cost.
        channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
        batch_size, in_planes, height, width = x.size()
        x = x * channel_attention
        x = x.reshape(1, -1, height, width)
        aggregate_weight = spatial_attention * kernel_attention * self.weight.unsqueeze(dim=0)
        aggregate_weight = torch.sum(aggregate_weight, dim=1).view(
            [-1, self.in_planes // self.groups, self.kernel_size, self.kernel_size])
        output = F.conv2d(x, weight=aggregate_weight, bias=None, stride=self.stride, padding=self.padding,
                          dilation=self.dilation, groups=self.groups * batch_size)
        output = output.view(batch_size, self.out_planes, output.size(-2), output.size(-1))
        output = output * filter_attention
        return output

    def _forward_impl_pw1x(self, x):
        channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
        x = x * channel_attention
        output = F.conv2d(x, weight=self.weight.squeeze(dim=0), bias=None, stride=self.stride, padding=self.padding,
                          dilation=self.dilation, groups=self.groups)
        output = output * filter_attention
        return output

    def forward(self, x):
        return self._forward_impl(x)

四、ODConv的官方代码报错解决

上面的代码你直接加入到模型中会报错有一处的Batchnorm会导致通道数报错(这里我不知道是我的版本原因还是怎么样当我觉得是官方的设计有问题),所以我将哪一个BN给删除了,在外层套了一个类解决了这一问题,下面的代码复制粘贴到 "ultralytics/nn/modules/conv.py"该文件的末尾。

class ODConv2d_yolo(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=1, stride=1, groups=1, dilation=1):
        super().__init__()
        self.conv = Conv(in_channels, out_channels, k=1)
        self.dcnv3 = ODConv2d(out_channels,out_channels, kernel_size=kernel_size, stride=stride, groups=groups,
                                   dilation=dilation)
        self.bn = nn.BatchNorm2d(out_channels)
        self.gelu = nn.GELU()

    def forward(self, x):
        x = self.conv(x)

        x = self.dcnv3(x)

        x = self.gelu(self.bn(x))
        return x

通过上面的代码我解决了官方代码中的报错。 

五、修改了ODConv的C2f和Bottleneck模块

下面的代码就是修改了ODConv的C2f和Bottleneck的代码,大家可以将其复制粘贴到如下的文件“ultralytics/nn/modules/block.py”的末尾处,

class Bottleneck_ODConv(nn.Module):
    """Standard bottleneck."""

    def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
        """Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
        expansion.
        """
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, k[0], 1)
        self.cv2 = ODConv2d_yolo(c_, c2, k[1], 1, groups=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        """'forward()' applies the YOLO FPN to input data."""
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))


class C2f_ODConv(nn.Module):
    """Faster Implementation of CSP Bottleneck with 2 convolutions."""

    def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
        """Initialize CSP bottleneck layer with two convolutions with arguments ch_in, ch_out, number, shortcut, groups,
        expansion.
        """
        super().__init__()
        self.c = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, 2 * self.c, 1, 1)
        self.cv2 = Conv((2 + n) * self.c, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.ModuleList(Bottleneck_ODConv(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))

    def forward(self, x):
        """Forward pass through C2f layer."""
        y = list(self.cv1(x).chunk(2, 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

    def forward_split(self, x):
        """Forward pass using split() instead of chunk()."""
        y = list(self.cv1(x).split((self.c, self.c), 1))
        y.extend(m(y[-1]) for m in self.m)
        return self.cv2(torch.cat(y, 1))

六、在模型中调用ODConv模块

上面我们只是修改了各种模块,那么如何在我们的模型中用到上面的修改,这里考虑有的人是新手有的人已经很清楚修改流程了,同时为了保证文章的可读性和流畅程度(因为修改教程需要放一堆图片大家看着也不舒服),专门开了一个文章讲述如何修改yolov8的yaml文件在模型中应用ODConv模块,如果你不知道如何用就看以下的文章里面讲述了我们在模型中修改的模块该如何进行更改到模型的配置中, 如果你已经熟练如何更改了可以不用阅读下面的文章。

YOLOv8改进 | 如何在网络结构中添加注意力机制、C2f、卷积、Neck、检测头

七、全文总结 

到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv8改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,目前本专栏免费阅读(暂时,大家尽早关注不迷路~),如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~

本专栏其它内容(持续更新) 

YOLOv8改进有效涨点系列->手把手教你添加动态蛇形卷积(Dynamic Snake Convolution)

YOLOv8性能评估指标->mAP、Precision、Recall、FPS、IoU

YOLOv8改进有效涨点系列->适合多种检测场景的BiFormer注意力机制(Bi-level Routing Attention)

 YOLOv8改进有效涨点系列->多位置替换可变形卷积(DCNv1、DCNv2、DCNv3) 

详解YOLOv8网络结构/环境搭建/数据集获取/训练/推理/验证/导出/部署

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/155162.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu22.04离线安装uwsgi问题记录

GCC4.8安装 1、报错信息1: 由于缺少gcc4.8环境导致的无法安装uwsgi 解决方案: 离线安装GCC4.8环境, GCC4.8.5离线安装步骤如下: 1、下载gcc的离线安装包及其依赖包,链接如下: https://download.csdn.net/download/…

苹果MAC安装绿盾出现问题,安装时没有出现填服务器地址的页面,现在更改不了也卸载不了绿盾 怎么处理?

环境: Mac mini M1 Mac os 11.0 绿盾v6.5 问题描述: 苹果MAC安装绿盾出现问题,安装时没有出现填服务器地址的页面,现在更改不了也卸载不了绿盾 怎么处理? 解决方案: 大部分企业是Windows和Mac终端混合使用,在进行文档加密管理时通常会遇到不兼容的现象,而为了统一…

Google codelab WebGPU入门教程源码<2> - 绘制几何形状(源码)

对应的教程文章: https://codelabs.developers.google.com/your-first-webgpu-app?hlzh-cn#3 源码执行效果: 对应的教程源码: 此处源码和教程本身提供的部分代码可能存在一点差异。 class Color4 {r: number;g: number;b: number;a: number;constructor(pr 1.0, pg 1.0, …

C++初阶 类和对象(下)

目录 一、拷贝构造函数 1.1 什么是拷贝构造函数? 1.2 为什么得是引用? 1.3 使用拷贝构造函数 1.4 拷贝构造函数有什么用? 二、运算符重载 2.1 什么是运算符重载? 2.2 尝试前须知 2.3 常见运算符重载 2.3.1运算符重载 …

树莓派通过网线连接电脑(校园网也能连接),实现SSH连接

前言 之前通过串口登入树莓派,太麻烦,通过网络登入树莓派,学校校园网又连接不了,想起来可以使用网线连接树莓派和电脑。 目录 树莓派通过网线连接电脑思路分析 树莓派通过网线连接电脑实现 1.硬件需求 2.打开Windows的网络 …

12.Oracle的索引

Oracle11g的索引 一、什么是索引二、索引的分类三、索引的语法四、分析索引四、索引的作用及使用场景 一、什么是索引 在Oracle数据库中,索引是一种特殊的数据结构,用于提高查询性能和加速数据检索。索引存储了表中某列的值和对应的行指针,这…

2023.11.15 关于 Spring Boot 配置文件

目录 引言 Spring Boot 配置文件 properties 配置文件说明 基本语法 读取配置文件 优点 缺点 yml 配置文件说明 基本语法 读取配置文件 yml 配置不同数据数据类型及 null 字符串 加单双引号的区别 yml 配置 列表(List) 和 映射(…

WordPress丸子小程序问题常见解决方案

WordPress丸子小程序问题常见解决方案 下载uniapp,导入项目。修改下图域名为自己的WP博客地址。(如下图) 发行微信小程序,如果显示不是项目所有者,请重新获取。 重新发行微信小程序,请打开微信开发者工具端…

MySql分区

一、什么是分区 MySQL分区是一种数据库设计和管理技术,它允许你将表分割成独立的、具有特定规则的存储单元。每个分区可以独立地进行管理,包括备份、恢复和优化。分区的主要目的是提高查询性能、简化维护以及实现数据的更有效管理。 以下是MySQL分区的…

深度解析:ERP管理系统背后的逻辑

深度解析:ERP管理系统背后的逻辑企业运营日益复杂化,一套高效且实用的ERP管理系统成为了企业成功的关键因素之一。然而,许多企业对于ERP管理系统的理解仍停留在表面,并未深入理解其背后的逻辑。本文将为您深度解析ERP管理系统背后…

腾讯云服务器价格表查询,腾讯云服务器怎么买便宜?

你是否需要搭建一个属于自己的网站,但是又不知道该如何选择和购买服务器?腾讯云服务器价格表查询,让你轻松了解各款服务器的价格及配置信息,帮助你选择最合适的服务器。同时,我们还为你提供了腾讯云服务器的优惠购买入…

main.js 中的 render函数

按照之前的单组件文件中的写法&#xff0c;我们的写法应该是这样的 import App from ./App.vuenew Vue({el: #app,templete: <App></App>,components: {App}, }) 1、定义el根节点。2、注册App组件。3、渲染 templete 模板 但是在脚手架工程中&#xff0c;他是这…

kubernetes--数据存储

目录 一、数据存储引言&#xff1a; 二、基础存储卷&#xff1a; 1. emptyDir存储卷&#xff1a; 2. hostPath存储卷&#xff1a; 3. nfs共享存储卷&#xff1a; 3.1 配置nfs: 3.2 master节点编写yaml文件&#xff1a; 4. 总结&#xff1a; 三、PVC和PV&#xff1a; 1. PV 的…

Jmeter- Beanshell语法和常用内置对象(网络整理)

在利用jmeter进行接口测试或者性能测试的时候&#xff0c;我们需要处理一些复杂的请求&#xff0c;此时就需要利用beanshell脚本了&#xff0c;BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法&#xff0c;所以它和java是可以无缝衔接的。beans…

python趣味编程-5分钟实现一个测验应用程序(含源码、步骤讲解)

Python测验是用 Python 编程语言编写的,这个关于 Python 编程的简单测验是一个简单的项目,用于测试一个人在给定主题考试中的知识能力。 Python 中的 Quiz项目仅包含用户端。用户必须先登录或注册才能开始Python 测验。 此外,还规定了解决问题的时间。用户应在时间结束前解…

VIM去掉utf-8 bom头

Windows系统的txt文件在使用utf-8编码保存时会默认在文件开头插入三个不可见的字符&#xff08;0xEF 0xBB 0xBF&#xff09;称为BOM头 BOM头文件 0.加上BOM标记&#xff1a; :set bomb 1.查询当前UTF-8编码的文件是否有BOM标记&#xff1a; :set bomb? :set bomb? 2.BOM头:文…

C语言的动态内存管理

目录 一、malloc函数 二、free函数 三、calloc函数 四、realloc函数 五、realloc函数原地扩容和异地扩容测试 六、动态内存管理的注意事项 一、malloc函数 1.头文件&#xff1a;stdlib.h&#xff08;malloc.h&#xff09; 2.函数原型&#xff1a;void * malloc(size_t siz…

STM32_SPI总线驱动OLED详细原理讲解

目录 这里写目录标题 第13章 Cortex-M4-SPI总线13.1 SPI总线概述13.1.1 SPI总线介绍13.1.2 SPI总线接口与物理拓扑结构13.1.3 SPI总线通信原理13.1.4 SPI总线数据格式 13.2 IO口模拟SPI操作OLED13.2.1 常见的显示设备13.2.2 OLED显示屏概述13.2.3 OLED特征13.2.4 显示原理13.2.…

模拟实现一个Linux中的简单版shell

exec系列接口中的环境变量 在之前我们学习了exec系类函数的功能就是将一个程序替换成另外一个程序。 然后就会出现下面的问题&#xff1a; 首先父进程对应的环境变量的信息是从bash中来的&#xff0c;因为我们自己写的父进程在运行的时候首先就要成为bash的子进程。这里我们将…

基于单片机的温度控制器系统设计

**单片机设计介绍&#xff0c; 基于单片机的温度控制器系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的温度控制器系统是一种利用单片机来检测环境温度并控制温度的系统。它通常由以下几个部分组成&#xff…