结合具体代码理解yolov5-7.0锚框(anchor)生成机制

news2024/11/29 8:36:19

最近对yolov5-7.0的学习有所深入,感觉官方代码也比较易读,所以对网络结构的理解更进一步,其中对锚框生成这块没太看明白细节,也想弄明白这块,于是前前后后好好看了代码。现在把我的学习收获做一下记录。个人见解,如有问题欢迎指正

1、梳理一下锚框机制

锚框(anchors),先验框,预选框,说的都是一个玩意,就是在输入数据经过特征提取阶段,一般都是做下采样降低数据量得到高层特征图,再在这些高层特征图上预设锚框与标签ground truth进行损失计算,根据梯度反向传播更新网络的参数,逐渐迭代使网络的参数可以直接识别出目标的位置和类别,最后把更新的具有最好识别效果的参数进行保存就得到了网络对识别某类目标的网络权重文件。

所以,锚框的预设一般在特征提取的末个阶段,这个阶段生成低分辨率的特征图,在yolov5s中,就是最后一层Detect生成的80 x 80, 40 x 40, 20 x 20的特征图,随后在特征图的基础上预设锚框。关于锚框,可以直接看到的信息是yolov5s.yaml中的
在这里插入图片描述

2、锚框具体是如何作用的

那这个锚框具体是如何作用的呢?下面看一下提取特征的最后一个阶段Detect的代码(models/yolo.py)

class Detect(nn.Module):
    # YOLOv5 Detect head for detection models
    stride = None  # strides computed during build
    dynamic = False  # force grid reconstruction
    export = False  # export mode

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.no = nc + 5  # number of outputs per anchor
        self.nl = len(anchors)  # number of detection layers
        self.na = len(anchors[0]) // 2  # number of anchors
        self.grid = [torch.empty(0) for _ in range(self.nl)]  # init grid
        self.anchor_grid = [torch.empty(0) for _ in range(self.nl)]  # init anchor grid
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  # shape(nl,na,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  # output conv
        self.inplace = inplace  # use inplace ops (e.g. slice assignment)

    def forward(self, x):
        z = []  # inference output
        for i in range(self.nl):
            x[i] = self.m[i](x[i])  # conv
            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()

            if not self.training:  # inference
                if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
                    self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

                if isinstance(self, Segment):  # (boxes + masks)
                    xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)
                    xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i]  # xy
                    wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
                else:  # Detect (boxes only)
                    xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
                    xy = (xy * 2 + self.grid[i]) * self.stride[i]  # xy
                    wh = (wh * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, conf), 4)
                z.append(y.view(bs, self.na * nx * ny, self.no))

        return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)

    def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, '1.10.0')):
        d = self.anchors[i].device
        t = self.anchors[i].dtype
        shape = 1, self.na, ny, nx, 2  # grid shape
        y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
        yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x)  # torch>=0.7 compatibility
        grid = torch.stack((xv, yv), 2).expand(shape) - 0.5  # add grid offset, i.e. y = 2.0 * x - 0.5
        anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
        return grid, anchor_grid

可以看见Detect类有anchors参数输入,说明anchors的 信息在这个类进行了利用,类中与anchors相关的代码一个是

self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  # shape(nl,na,2)

另一个是forward函数下的_make_grid函数。但是forward函数在训练阶段并不执行这个函数,所以这就不太能看明白锚框是怎么在特征图上生成的。

if not self.training:  # inference
    if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
    	self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

那么Detect类训练阶段执行的代码就是:

class Detect(nn.Module):
    # YOLOv5 Detect head for detection models
    stride = None  # strides computed during build
    dynamic = False  # force grid reconstruction
    export = False  # export mode

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.no = nc + 5  # number of outputs per anchor
        self.nl = len(anchors)  # number of detection layers
        self.na = len(anchors[0]) // 2  # number of anchors
        self.grid = [torch.empty(0) for _ in range(self.nl)]  # init grid
        self.anchor_grid = [torch.empty(0) for _ in range(self.nl)]  # init anchor grid
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  # shape(nl,na,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  # output conv
        self.inplace = inplace  # use inplace ops (e.g. slice assignment)

    def forward(self, x):
        z = []  # inference output
        for i in range(self.nl):
            x[i] = self.m[i](x[i])  # conv
            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
        
        return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)

这就不得不说这个register_buffer()函数,这里只有他和 anchors亲密接触了,此前我后续看了把Detect类包含起来的BaseModel和DetectionModel类,都没有看出涉及铺设锚框的操作。

3、register_buffer()函数

原来,register_buffer()函数可以把 anchors 作为参数固定到网络中,并且该函数传入的参数不随训练迭代改变,而且在网络训练结束时随模型保存输出。这可以看看register_buffer()函数和register_parameter()函数、nn.Parameter()、model.state_dict()以及model.parameters()、model.buffers()的功能和区别。可见Pytorch模型中的parameter与buffer

我调试做的记录如下:

class Detect(nn.Module):
    # YOLOv5 Detect head for detection models
    stride = None  # strides computed during build
    dynamic = False  # force grid reconstruction
    export = False  # export mode

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        super().__init__()
        self.nc = nc  # number of classes
        self.no = nc + 5  # number of outputs per anchor
        self.nl = len(anchors)  # number of detection layers
        self.na = len(anchors[0]) // 2  # number of anchors
        self.grid = [torch.empty(0) for _ in range(self.nl)]  # init grid
        self.anchor_grid = [torch.empty(0) for _ in range(self.nl)]  # init anchor grid
        self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2))  # shape(nl,na,2)
        """anchors"""
        """ register_buffer的参数不参与梯度更新,最终随模型保存输出,此处把传入的anchors参数的数据通过register_buffer传入到网络中,"""
        """torch.tensor(anchors).float()
                tensor([[ 10.,  13.,  16.,  30.,  33.,  23.],
                        [ 30.,  61.,  62.,  45.,  59., 119.],
                        [116.,  90., 156., 198., 373., 326.]])
                ipdb> torch.tensor(anchors).float().view(self.nl, -1, 2)
                tensor([[[ 10.,  13.],
                        [ 16.,  30.],
                        [ 33.,  23.]],

                        [[ 30.,  61.],
                        [ 62.,  45.],
                        [ 59., 119.]],

                        [[116.,  90.],
                        [156., 198.],
                        [373., 326.]]])
                ipdb> self.anchor_grid
                [tensor([]), tensor([]), tensor([])]
                ipdb> anchors
                [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
                """
        # import ipdb;ipdb.set_trace()
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  # output conv
        self.inplace = inplace  # use inplace ops (e.g. slice assignment)

        """ Detect层对输入的三个下采样倍数的数据分别采用三个全连接层输出
        self.m=
            ModuleList(
            (0): Conv2d(128, 18, kernel_size=(1, 1), stride=(1, 1))
            (1): Conv2d(256, 18, kernel_size=(1, 1), stride=(1, 1))
            (2): Conv2d(512, 18, kernel_size=(1, 1), stride=(1, 1))
            )
            其中输出维度 self.no * self.na,即此处的 18,表示每个维度三种尺度的锚框 x ( 类别 + xywh + score) = 3 x 6
        """
        
    def forward(self, x):
        """self.state_dict()
                anchors
                m.0.weight
                m.0.bias
                m.1.weight
                m.1.bias
                m.2.weight
                m.2.bias
        """
        
        """对应8,16,32倍下采样输出的特征图
            x:  x[0].shape
                torch.Size([1, 128, 32, 32]) 
                ipdb> x[1].shape
                torch.Size([1, 256, 16, 16])
                ipdb> x[2].shape
                torch.Size([1, 512, 8, 8])
        """
        z = []  # inference output
        import ipdb;ipdb.set_trace()
        for i in range(self.nl):
            x[i] = self.m[i](x[i])  # conv
            """经过全连接层后数据维度
            x[0].shape 
                        = torch.Size([1, 18, 32, 32])
                x[1].shape 
                        = torch.Size([1, 18, 16, 16])
                x[2].shape 
                        = torch.Size([1, 18, 8, 8])
            """
            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            
            """训练阶段 对应8,16,32倍下采样输出的特征图在Detect类输出的数据维度
                x:x[0].shape
                        torch.Size([1, 3, 32, 32, 6])
                        ipdb> x[1].shape
                        torch.Size([1, 3, 16, 16, 6])
                        ipdb> x[2].shape
                        torch.Size([1, 3, 8, 8, 6])
            """
            
            if not self.training:  # inference
                if self.dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
                    self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)

                if isinstance(self, Segment):  # (boxes + masks)
                    xy, wh, conf, mask = x[i].split((2, 2, self.nc + 1, self.no - self.nc - 5), 4)
                    xy = (xy.sigmoid() * 2 + self.grid[i]) * self.stride[i]  # xy
                    wh = (wh.sigmoid() * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, conf.sigmoid(), mask), 4)
                else:  # Detect (boxes only)
                    xy, wh, conf = x[i].sigmoid().split((2, 2, self.nc + 1), 4)
                    """推断流程预测xywh"""
                    xy = (xy * 2 + self.grid[i]) * self.stride[i]  # xy
                    wh = (wh * 2) ** 2 * self.anchor_grid[i]  # wh
                    y = torch.cat((xy, wh, conf), 4)
                z.append(y.view(bs, self.na * nx * ny, self.no))

        return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)

    def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, '1.10.0')):
        d = self.anchors[i].device
        t = self.anchors[i].dtype
        shape = 1, self.na, ny, nx, 2  # grid shape
        y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
        yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x)  # torch>=0.7 compatibility
        grid = torch.stack((xv, yv), 2).expand(shape) - 0.5  # add grid offset, i.e. y = 2.0 * x - 0.5
        anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
        return grid, anchor_grid

上面的调试情况也可以充分说明锚框通过参数形式注册到网络中,在forward()函数中,我看了self,即该Detect类结构的参数情况,其中self.state_dict() ------> anchors, m.0.weight, m.0.bias, m.1.weight, m.1.bias, m.2.weight, m.2.bias就包含了anchors。

4、锚框的具体生成

那么锚框的具体生成就是:

def forward(self, x):
"""输入x是进入Detect的三个尺度的特征图"""
        z = []  # inference output
        for i in range(self.nl):
        """self.nl=3表示Detect对应三个尺度用以处理三层特征图的网络结构,
        	self.m是对应三个尺度特征图的网络结构,对应不同的输入数据维度,输出维度都是18,其中输出维度 self.no * self.na,即此处的 18,表示每个维度三种尺度的锚框 x ( 类别 + xywh + score) = 3 x 6
        	self.m=
            ModuleList(
            (0): Conv2d(128, 18, kernel_size=(1, 1), stride=(1, 1))
            (1): Conv2d(256, 18, kernel_size=(1, 1), stride=(1, 1))
            (2): Conv2d(512, 18, kernel_size=(1, 1), stride=(1, 1))
            )"""
            x[i] = self.m[i](x[i])  # conv
            """对应的输入特征图在对应的m的网络结构中进行计算
            	x[0].shape 
                        = torch.Size([1, 18, 32, 32])
                x[0].shape 
                        = torch.Size([1, 18, 16, 16])
                x[2].shape 
                        = torch.Size([1, 18, 8, 8])"""
            bs, _, ny, nx = x[i].shape  # x(bs,255,20,20) to x(bs,3,20,20,85)
            x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
            """改变数据输出维度便于后续处理
            	x:x[0].shape
                        torch.Size([1, 3, 32, 32, 6])
                        ipdb> x[1].shape
                        torch.Size([1, 3, 16, 16, 6])
                        ipdb> x[2].shape
                        torch.Size([1, 3, 8, 8, 6])
                 维度变换:1表示batch_size;3表示3种尺度的锚框;32,32表示特征图维度;6表示预测结果,含类别 + x、y、w、h + score
            """

看一下yolov5s.yaml的anchors信息

anchors:
  - [10,13, 16,30, 33,23]  # P3/8
  - [30,61, 62,45, 59,119]  # P4/16
  - [116,90, 156,198, 373,326]  # P5/32

其中,10,13;16,30;33,23分别表示预设的三种尺度的锚框,表示锚框的宽高尺度,另两行同理,分别对应8倍、16倍和32倍下采样倍数的输出特征图的预设锚框,所以每种尺度的输出特征图都会生成三种尺度的锚框的输出特征图,三种尺度的特征图共生成9种尺度的锚框。

所以,数据经过Detect类已经在特征图的基础上做了预测,也就可以理解看到一些解说网络在目标的位置预测上是预测位置的偏移量,因为是特征图数据乘以预设锚框参数的宽高得到目标的位置,并不是直接预测目标的位置。

其中,在获取数据的锚框信息时,实际上是在BaseModel类的_forward_once
函数中处理数据信息的:

class DetectionModel(BaseModel):
	......
	def forward(self, x, augment=False, profile=False, visualize=False):
        if augment:
            return self._forward_augment(x)  # augmented inference, None
        return self._forward_once(x, profile, visualize)  # single-scale inference, train

class BaseModel(nn.Module):
    # YOLOv5 base model
    def forward(self, x, profile=False, visualize=False):
        return self._forward_once(x, profile, visualize)  # single-scale inference, train

    def _forward_once(self, x, profile=False, visualize=False):
        y, dt = [], []  # outputs
        for m in self.model:
        """self.model是构建好的网络结构,输入x是实际数据"""
            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]  # from earlier layers
            if profile:
                self._profile_one_layer(m, x, dt)
            x = m(x)  # run
            y.append(x if m.i in self.save else None)  # save output
            if visualize:
                feature_visualization(x, m.type, m.i, save_dir=visualize)
        return x
        .......

这里输出的x与train.py里输出的pred是等效的

with torch.cuda.amp.autocast(amp):
    pred = model(imgs)  # forward
    loss, loss_items = compute_loss(pred, targets.to(device))  # loss scaled by batch_size```

若果要可视化锚框,建议对pred进行处理,具体处理方式建议参考Detect类中not training下的代码,我初步试过,是可以画出来的。

如果采用预训练模型,可能锚框显示效果不一定好,因为预训练模型已经经过一定数据的训练,网络的参数对目标具有一定的识别能力。

5、总结

在yolov5s中,anchors是通过register_buffer()函数把预设的锚框尺度信息作为参数注册到最后的Detect网络层中。因为目标的位置信息是 锚框参数 乘以 特征图数据 得到,所以网络预测的是一种相对锚框的位置信息,可以理解成是锚框的位置偏移量,网络通过学习预设锚框针对目标的位置偏移量来实现目标的类别识别和位置预测。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在大规模服务中迁移缓存

当您启动初始服务时,通常会过度设计以考虑大量流量。但是,当您的服务达到爆炸式增长阶段,或者如果您的服务请求和处理大量流量时,您将需要重新考虑您的架构以适应它。糟糕的系统设计导致难以扩展或无法满足处理大量流量的需求&…

第三章 decimal模块

1. decimal 模块介绍 decimal 模块是 Python 提供的用于进行十进制定点和浮点运算的内置模块。使用它可以快速正确地进行十进制定点和浮点数的舍入运算,并且可以控制有效数字的个数。 使用 decimal 模块主要是因为它与 Python 自带的浮点数相比,有以下…

关于Stream流和Lambda表达式,这些技巧你都知道吗?

💧 关于 S t r e a m 流和 L a m b d a 表达式,这些技巧你都知道吗? \color{#FF1493}{关于Stream流和Lambda表达式,这些技巧你都知道吗?} 关于Stream流和Lambda表达式,这些技巧你都知道吗?&…

2014年全国硕士研究生入学统一考试管理类专业学位联考写作试题

2014年1月真题: 四、写作:第56~57小题,共65分。其中论证有效性分析30 分,论说文35分。 56.论证有效性分析: 分析下述论证中存在的缺陷和漏洞,选择若干要点,写一篇600字左右的文章,对该论证的有效性进行分析和评论。…

马克思第二章

1.实践和认识 实践决定认识,认识又反作用于实践 实践的特点: 1.直接现实性 2.自觉能动性 3.社会历史性 实践和认识的关系 1.实践是认识的来源 2.实践是认识的目的 3.实践是认识的发展动力 4.实践是检验认识真理的唯一标准 5.认识又反作用于实践&#xf…

gocv Windows10下编译和安装(opencv4.7)

opencv居然还没有官方的golang版,出乎意料。为了编译安装这玩意,折腾了一下午,记录下: 资源提前下载 1、 MinGW-w64 这里的坑是对于只懂一点点的人,容易选错版本: 没仔细看的人很可能会选win32的&#x…

【EDA软件互转】PADS转Allegro

1. 使用pads软件打开PCB文件,然后执行菜单命令:File->Export->弹出的对话框中点击“保存”。然后按下图设置后点击“OK”按钮,会在当前目录下生成一个前面保存的asc文件;如果点击OK后有弹出其它提示就点击“确定”就好…

在编写测试报告的时候,我们有哪些点需要注意的呢?

测试报告作为测试人员的核心输出项,是体现自己工作价值的重要承载工具,需要我们认真对待,所以我们要重视测试报告的输出,那么在编写测试报告的时候,我们有哪些点需要注意的呢? 1、不要乱用模板 很多测试新…

CTR预估之WideDeep系列(下):NFM/xDeepFM

在上一篇文章中CTR预估之Wide&Deep系列模型:DeepFM/DCN,学习了Wide & Deep这种通用框架:wide组件的线性模型的显性低阶特征交叉提供记忆能力,deep组件的深度网络模型的隐式高阶特征交叉提供泛化能力,还有DeepFM和Deep&…

蓝桥杯刷题篇①

前言:hello各位童学们好呀!许久不见!本文为本人的蓝桥杯OJ的刷题笔记!文章隶属于专栏蓝桥杯,该专栏的目的是为了记录自己的刷题记录和学习过程,激励自己不断前行,为明年的ACM、ICPC、蓝桥杯等比…

CVPR2023最佳论文提名(12篇)

CVPR2023公布了12篇最佳论文候选文章。(直接点击标题可以查看原文~) Ego-Body Pose Estimation via Ego-Head Pose Estimation 单位:Stanford-----------关键词:姿态估计 3D Registration With Maximal Cliques 单位&…

三层交换机与路由互联配置(华为设备)

#三层交换机与路由器配置配置 #三层交换机与路由器配置配置 路由器配置 #进入系统视图 <Huawei>system-view #关闭系统提示信息 [Huawei]undo info-center enable #配置一个环回口 [Huawei]int LoopBack 0 #配置IP地址 与 掩码 [Huawei-LoopBack0]ip address 1.1.…

基于matlab使用自定义辐射方向图进行天线阵列分析(附源码)

一、前言 此示例演示如何使用自定义天线辐射方向图创建天线阵列&#xff0c;然后如何分析阵列的响应方向图。这种模式可以通过测量或模拟获得。 二、导入辐射图 根据应用的不同&#xff0c;实用的相控天线阵列有时会使用专门设计的天线元件&#xff0c;其辐射方向图无法用闭式方…

Java018——Java方法

什么是方法&#xff1f; 方法的作用&#xff1f; 方法的定义 方法的使用 一、什么是方法&#xff1f; Java方法是语句的集合 二、方法的作用&#xff1f; 它们&#xff08;语句&#xff09;在一起执行一个功能。 三、方法的定义 格式&#xff1a; 修饰符 返回值类型 方法名…

PostgreSQL(九)内置系统视图

目录 一、系统视图二、 pg_stat_activity 视图1.简介2.核心字段3.全部字段 一、系统视图 PGSQL 中提供了一系列内置的视图&#xff0c;包括系统视图和其他视图。 系统视图提供了查询系统表的一些便利的访问方法。其他视图提供了访问内部服务器状态的方法。 官方文档&#xf…

Spring Boot是什么?详解它的优缺点以及四大核心

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、Spring Boot 是什么&#xff1f; 二、Spring Boot 的优缺点 1、优点 ①可快速构建独立的 Spring 应用 ②直接嵌入Tomcat、Jett…

【Python GUI编程系列 01】安装python pycharm 和 pyside6

Python GUI编程系列 01 安装python pycharm 和 pyside61、安装python2、安装pycharm3、安装 pyside6 安装python pycharm 和 pyside6 本系列使用python3 pycharmpyside6 来进行python gui设计&#xff0c;首先我们来配置编程环境 PS&#xff1a;为了减少复杂程度&#xff0c;本…

学习HCIP的day.15

目录 三层架构 一、网络拓扑冗余 1、线路冗余 2、设备冗余 3、网关冗余 4、电源冗余 二、三和一&#xff08;网关、根网桥、SVI&#xff09; 三、管理vlan 四、三层交换机 五、网关冗余 六、名词注解&#xff1a; 七、数据交换方法&#xff1a; 1、原始交…

STM32开发——ADC(烟雾传感器)

目录 1.ADC简介 2.项目简介 3.CubeMX设置 4.函数代码 1.ADC简介 作用&#xff1a;用于读取电压值&#xff0c;然后转换为数字量传给单片机&#xff0c;单片机再通过计算&#xff0c;可以得到电压值。 ADC的性能指标 量程&#xff1a;能测量的电压范围分辨率&#xff1a;A…

简聊关于Flutter的争议~

关于Flutter的争议 Flutter是谷歌的推出的跨平台UI框架&#xff0c;可以快速在iOS和Android上构建高质量的原生用户界面&#xff0c;可以与现有的代码一起工作。在全世界&#xff0c;Flutter正在被越来越多的开发者和组织使用&#xff0c;并且Flutter是完全免费、开源的。这是…