下午好~ 我的论文【yolov5】(第四期)

文章目录

  • 简介
  • 模型
    • Mosaic数据增强
    • 自适应锚框计算
    • 自适应图片缩放
    • Focus结构
    • CSP结构 Neck
    • CIOU_Loss
    • nms非极大值抑制
    • 代码
    • 最后

简介

YOLO V4没过多久YOLO V5就出来了。YOLO V5的模型架构是与V4非常相近的。

模型

Yolov5官方代码中,给出的目标检测网络中一共有4个版本,分别是Yolov5s、Yolov5m、Yolov5l、Yolov5x四个模型。

Yolov5s整体的网络结构图:
在这里插入图片描述
图片来自:https://zhuanlan.zhihu.com/p/172121380

(1)输入端:Mosaic数据增强、自适应锚框计算、自适应图片缩放
(2)Backbone:Focus结构,CSP结构
(3)Neck:FPN+PAN结构
(4)Prediction:GIOU_Loss

Mosaic数据增强

Mosaic数据增强的步骤包括:

选择四张不同的图像:从训练数据集中随机选择四张图像。

随机选取一个位置:随机选择一个位置在这四张图像中,这个位置将成为新的合成图像的中心点。

将这四张图像拼接:将选中位置的像素从四张图像中裁剪出来,然后将它们拼接在一起,形成新的合成图像。

调整目标框坐标:如果图像中包含目标框(用于目标检测的边界框),则需要相应地调整这些目标框的坐标,以反映合成图像中目标的新位置。

在这里插入图片描述

自适应锚框计算

在Yolo算法中,针对不同的数据集,都会有初始设定长宽的锚框。

在Yolov3、Yolov4中,训练不同的数据集时,计算初始锚框的值是通过单独的程序运行的。

但Yolov5中将此功能嵌入到代码中,每次训练时,自适应的计算不同训练集中的最佳锚框值。

传统的目标检测方法中,锚框通常是在图像中均匀分布的一组预定义框,但这种方法可能无法很好地适应不同尺度、长宽比或者特定场景下目标的变化。为了更好地适应不同的场景,自适应锚框计算采用一种动态的策略,根据数据集中的实际目标分布情况,调整生成锚框的尺度和长宽比。

自适应锚框计算可以采用以下策略:

数据统计分析: 对训练数据集进行分析,了解不同目标的尺度和长宽比分布情况。

动态调整锚框: 根据数据统计的结果,动态地调整生成锚框的尺度和长宽比,使其更符合实际场景中的目标形状和大小变化。

学习适应性参数: 有些方法还可以通过学习适应性参数的方式,让模型自动调整生成锚框的尺度和长宽比。

在yolov5x.yaml中查看锚定框的数据,如下:

# yolov5中预先设定了锚定框,这些锚框是针对coco数据集的,其他目标检测也适用。
# 这些框针对的图片大小是640x640,是默认的anchor大小。
# 需要注意的是在目标检测任务中,一般使用大特征图上去检测小目标,因为大特征图含有更多小目标信息,
# 因此大特征图上的anchor数值通常设置为小数值,小特征图检测大目标,因此小特征图上anchor数值设置较大。
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   最小特征图上的锚框

在yolov5 中训练开始前,计算数据集标注信息针对默认锚定框的最佳召回率,当最佳召回率大于等于0.98时,则不需要更新锚定框;如果最佳召回率小于0.98,则需要重新计算数据集的锚定框,如果计算处理更好则更新原理的anchors。

def check_anchors(dataset, detect, thr=4.0, imgsz=640):"""# Check anchor fit to data, recompute if necessary检查 anchor你和数据情况, 如果不合理重新计算anchors输入:dataset - 数据集对象detect - 模型的最后一层,检测头thr - 阈值?imgsz - 图片大小"""m = detect# 例如: shape = 640 * [1280  960] / 1280shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)# augment scale 随机缩放系数scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1))# 计算框的whwh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float()# 计算指标 bpr,aatdef metric(k):  # compute metricr = wh[:, None] / k[None]x = torch.min(r, 1 / r).min(2)[0]  # ratio metricbest = x.max(1)[0]  # best_xaat = (x > 1 / thr).float().sum(1).mean()   # anchors above thresholdbpr = (best > 1 / thr).float().mean()       # best possible recallreturn bpr, aat# 计算BPR(最好的召回率)stride = m.stride.to(m.anchors.device).view(-1, 1, 1)   # model stridesanchors = m.anchors.clone() * stride                    # current anchorsbpr, aat = metric(anchors.cpu().view(-1, 2))s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). '# 如果召回率>0.98,当前的anchors是正常的# 否则就修正anchorsif bpr > 0.98:logging.info(f'{s}Current anchors are a good fit to dataset ✅')else:logging.info(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...')na = m.anchors.numel() // 2  # number of anchors# 基于kmean方法重新计算 anchorsanchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)new_bpr = metric(anchors)[0]# 如果新的anchors更好就替换if new_bpr > bpr:  # replace anchorsanchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)m.anchors[:] = anchors.clone().view_as(m.anchors)check_anchor_order(m)  # must be in pixel-space (not grid-space)m.anchors /= strides = f'{PREFIX}Done ✅ (optional: update model *.yaml to use these anchors in the future)'else:s = f'{PREFIX}Done ⚠️ (original anchors better than new anchors, proceeding with original anchors)'logging.info(s)

代码的主要步骤和解释:

  1. 计算框的wh:

    wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float()
    

    这一步计算了每个边界框(bounding box)的宽度(width)和高度(height),并将其保存在wh张量中。这些边界框的尺寸是通过将数据集中的标签(labels)的宽度和高度乘以随机缩放系数(scale)得到的。

  2. 定义度量函数(metric):

    def metric(k):  # compute metric# ... (略)
    

    metric函数用于计算度量指标,包括最佳可能召回率(Best Possible Recall,BPR)和锚框与目标之间的适应度。它使用wh和输入的锚框k计算这些度量。

  3. 计算当前锚框的适应度:

    stride = m.stride.to(m.anchors.device).view(-1, 1, 1)   # model strides
    anchors = m.anchors.clone() * stride                    # current anchors
    bpr, aat = metric(anchors.cpu().view(-1, 2))
    

    这一步将当前模型的锚框进行了一些处理,然后调用metric函数计算当前锚框的最佳可能召回率(BPR)和锚框数目相对于目标数目的比率(anchors/target)。这些度量值将用于判断当前锚框是否适合数据集。

  4. 判断是否需要修正锚框:

    if bpr > 0.98:logging.info(f'{s}Current anchors are a good fit to dataset ✅')
    else:# ... (略)
    

    如果当前锚框的最佳可能召回率(BPR)超过了0.98,说明当前锚框适合数据集。否则,将尝试修正锚框。

  5. 基于K均值(k-means)重新计算锚框:

    anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
    

    如果需要修正锚框,就调用kmean_anchors函数重新计算锚框。该函数使用K均值聚类方法来根据数据集的实际情况生成新的锚框。

  6. 比较新旧锚框的适应度:

    new_bpr = metric(anchors)[0]
    if new_bpr > bpr:  # replace anchors# ... (略)
    

    计算新锚框的最佳可能召回率(BPR),如果新锚框更好,则用新锚框替换模型中的锚框。

  7. 更新模型中的锚框:

    anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
    m.anchors[:] = anchors.clone().view_as(m.anchors)
    check_anchor_order(m)  # must be in pixel-space (not grid-space)
    m.anchors /= stride
    s = f'{PREFIX}Done ✅ (optional: update model *.yaml to use these anchors in the future)'
    

    将新的锚框复制到模型中,并进行一些必要的调整,确保锚框的顺序正确。更新完成后,输出一条日志消息,提示锚框已经更新。

基于kmean计算训练数据集新的anchors

def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):""" Creates kmeans-evolved anchors from training datasetArguments:dataset: path to data.yaml, or a loaded datasetn: number of anchorsimg_size: image size used for trainingthr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0gen: generations to evolve anchors using genetic algorithmverbose: print all resultsReturn:k: kmeans evolved anchorsUsage:from utils.autoanchor import *; _ = kmean_anchors()"""from scipy.cluster.vq import kmeansnpr = np.randomthr = 1 / thrdef metric(k, wh):  # compute metricsr = wh[:, None] / k[None]x = torch.min(r, 1 / r).min(2)[0]  # ratio metric# x = wh_iou(wh, torch.tensor(k))  # iou metricreturn x, x.max(1)[0]  # x, best_xdef anchor_fitness(k):  # mutation fitness_, best = metric(torch.tensor(k, dtype=torch.float32), wh)return (best * (best > thr).float()).mean()  # fitnessdef print_results(k, verbose=True):k = k[np.argsort(k.prod(1))]  # sort small to largex, best = metric(k, wh0)bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n  # best possible recall, anch > thrs = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \f'past_thr={x[x > thr].mean():.3f}-mean: 'for x in k:s += '%i,%i, ' % (round(x[0]), round(x[1]))if verbose:logging.info(s[:-2])return kif isinstance(dataset, str):  # *.yaml filewith open(dataset, errors='ignore') as f:data_dict = yaml.safe_load(f)  # model dictfrom utils.dataloaders import LoadImagesAndLabelsdataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)# Get label whshapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh# Filteri = (wh0 < 3.0).any(1).sum()if i:logging.info(f'{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size')wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32)  # filter > 2 pixels# wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1)  # multiply by random scale 0-1# Kmeans inittry:logging.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...')assert n <= len(wh)  # apply overdetermined constraints = wh.std(0)  # sigmas for whiteningk = kmeans(wh / s, n, iter=30)[0] * s  # pointsassert n == len(k)  # kmeans may return fewer points than requested if wh is insufficient or too similarexcept Exception:logging.warning(f'{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init')k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size  # random initwh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))k = print_results(k, verbose=False)# Plot# k, d = [None] * 20, [None] * 20# for i in tqdm(range(1, 21)):#     k[i-1], d[i-1] = kmeans(wh / s, i)  # points, mean distance# fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)# ax = ax.ravel()# ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')# fig, ax = plt.subplots(1, 2, figsize=(14, 7))  # plot wh# ax[0].hist(wh[wh[:, 0]<100, 0],400)# ax[1].hist(wh[wh[:, 1]<100, 1],400)# fig.savefig('wh.png', dpi=200)# Evolvef, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1  # fitness, generations, mutation prob, sigmapbar = tqdm(range(gen), bar_format=TQDM_BAR_FORMAT)  # progress barfor _ in pbar:v = np.ones(sh)while (v == 1).all():  # mutate until a change occurs (prevent duplicates)v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)kg = (k.copy() * v).clip(min=2.0)fg = anchor_fitness(kg)if fg > f:f, k = fg, kg.copy()pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'if verbose:print_results(k, verbose)return print_results(k).astype(np.float32)

函数kmean_anchors用于生成目标检测模型中的锚框(anchors)。主要步骤包括使用K均值(kmeans)聚类方法初始化锚框,然后通过遗传算法(genetic algorithm)进行锚框的进化。以下是代码的主要解释:

  1. 导入库:

    from scipy.cluster.vq import kmeans
    

    导入了SciPy库中用于K均值聚类的函数。

  2. 定义函数kmean_anchors

    def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
    

    这个函数接受多个参数,包括数据集路径(或加载的数据集对象)、锚框数量、图像大小、阈值、进化代数和是否输出详细信息等。

  3. 数据处理和准备:

    npr = np.random
    thr = 1 / thr
    

    设置一些随机种子和计算阈值的倒数。

  4. 度量计算函数metric

    def metric(k, wh):  # compute metrics# ... (略)
    

    该函数用于计算度量指标,包括比率指标和最佳可能召回率。

  5. 适应度函数anchor_fitness

    def anchor_fitness(k):  # mutation fitness# ... (略)
    

    该函数计算基于变异的适应度,用于评估锚框的质量。

  6. 结果打印函数print_results

    def print_results(k, verbose=True):# ... (略)
    

    该函数用于打印和返回计算结果,包括最佳可能召回率、锚框数量、锚框的平均度量值等。

  7. 加载数据集:

    if isinstance(dataset, str):  # *.yaml filewith open(dataset, errors='ignore') as f:data_dict = yaml.safe_load(f)  # model dictfrom utils.dataloaders import LoadImagesAndLabelsdataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
    

    如果输入的数据集是字符串路径,就加载数据集。

  8. 获取标签尺寸信息:

    shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
    wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh
    

    从数据集中获取标签的尺寸信息,并计算标签的宽度和高度。

  9. 过滤极小的目标:

    i = (wh0 < 3.0).any(1).sum()
    if i:logging.info(f'{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size')
    wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32)  # filter > 2 pixels
    

    过滤掉宽度或高度小于3像素的目标,并保留宽度或高度大于等于2像素的目标。

  10. K均值初始化锚框:

    try:logging.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...')assert n <= len(wh)  # apply overdetermined constraints = wh.std(0)  # sigmas for whiteningk = kmeans(wh / s, n, iter=30)[0] * s  # pointsassert n == len(k)  # kmeans may return fewer points than requested if wh is insufficient or too similar
    except Exception:logging.warning(f'{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init')k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size  # random init
    

    尝试使用K均值方法初始化锚框,如果出现异常(例如,聚类数目少于要求或标签信息不足),则切换到随机初始化锚框。

  11. 锚框数据准备:

    wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))
    k = print_results(k, verbose=False)
    

    将计算得到的锚框和相关数据准备成PyTorch张量,并进行一次结果打印。

  12. 锚框进化(遗传算法):

    f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1  # fitness, generations, mutation prob, sigma
    pbar = tqdm(range(gen), bar_format=TQDM_BAR_FORMAT)  # progress bar
    for _ in pbar:v = np.ones(sh)while (v == 1).all():  # mutate until a change occurs (prevent duplicates)v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)kg = (k.copy() * v).clip(min=2.0)fg = anchor_fitness(kg)if fg > f:f, k = fg, kg.copy()pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'if verbose:print_results(k, verbose)
    

    使用遗传算法对锚框进行进化,不断变异和评估锚框的适应度,最终得到适应度最好的锚框。

  13. 返回最终锚框:

    return print_results(k).astype(np.float32)
    

    返回经过进化算法得到的最终锚框,并打印最终结果。

自适应图片缩放

YOLOV5中相比于之前的版本,有很多小trick,导致其性能和应用比较好。

在目标检测中,输入的图片尺寸有大有小,根据前人的实验结果,输入网络的尺寸统一缩放到同一个尺寸时,检测效果会更好。

简单的使用resize操作可能导致信息的丢失,这主要是因为在改变图像的尺寸时,像素值的重新分布和插值操作可能引入失真。

插值操作: 在resize过程中,由于目标尺寸与原始尺寸不匹配,算法需要估计新尺寸上每个像素的值。常见的插值算法包括最近邻插值、双线性插值和双立方插值。这些插值方法都引入了一定的信息损失。

失真和形变: 简单的resize可能导致图像的失真和形变,特别是在缩小尺寸的情况下。这可能导致对象的形状发生变化,从而影响后续的视觉任务。

分辨率丧失: 缩小图像尺寸会导致分辨率的降低,从而丢失一些细节信息。

这时就有个问题,如果是简单的使用resize,很有可能就造成了图片信息的丢失,所以提出了letterbox自适应图片缩放技术。

train中放入的图片并不经过letterbox,而是检测的时候使用letterbox。

Letterbox 这个词的字面意思是“信箱”或“邮箱”,通常是指门或围栏上的一个盒子,用于接收信件或其他物品。在摄影和图像处理的上下文中,letterbox 这个词经常被用来描述一种图像调整的方法,该方法通过在图像周围添加边框(类似于信箱的概念)来适应不同的尺寸或宽高比例,以确保图像在显示或处理过程中不会失真或变形。

letterbox的主要思想是尽可能的利用网络感受野的信息特征。比如在YOLOV5中最后一层的Stride=5,即最后一层的特征图中每个点,可以对应原图中32X32的区域信息,那么只要在保证整体图片变换比例一致的情况下,长宽均可以被32整除,那么就可以有效的利用感受野的信息。

具体来说,图片变换比例一致指的是,长宽的收缩比例应该采用相同的比例。有效利用感受野信息则指对于收缩后不满足条件的一边,用灰白填充至可以被感受野整除。下面举例说明。

假设图片原来尺寸为(1080, 1920),我们想要resize的尺寸为(640,640)。要想满足收缩的要求,应该选取收缩比例640/1920 = 0.33.则图片被缩放为(360,640).下一步则要填充灰白边至360可以被32整除,则应该填充至384,最终得到图片尺寸(384,640)

而其实letterbox的实现也十分简单,以下将结合代码讲解步骤。

def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, stride=32):# Resize and pad image while meeting stride-multiple constraintsshape = im.shape[:2]  # current shape [height, width]if isinstance(new_shape, int):new_shape = (new_shape, new_shape)# Scale ratio (new / old)r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])# Compute paddingratio = r, r  # width, height ratiosnew_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh paddingif auto:  # minimum rectangledw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh paddingdw /= 2  # divide padding into 2 sidesdh /= 2print(dw, dh)if shape[::-1] != new_unpad:  # resizeim = cv.resize(im, new_unpad, interpolation=cv.INTER_LINEAR)top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))left, right = int(round(dw - 0.1)), int(round(dw + 0.1))im = cv.copyMakeBorder(im, top, bottom, left, right, cv.BORDER_CONSTANT, value=color)  # add borderreturn im, ratio, (dw, dh)

letterbox的函数,用于对输入的图像进行缩放和填充,以满足指定的尺寸要求和stride要求。

  1. 获取输入图像的形状:

    shape = im.shape[:2]  # current shape [height, width]
    

    这行代码获取输入图像的高度和宽度。

  2. 处理new_shape参数:

    if isinstance(new_shape, int):new_shape = (new_shape, new_shape)
    

    如果new_shape是整数,将其转换为元组。

  3. 计算缩放比例:

    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    

    计算缩放比例,以确保图像适应新的尺寸,同时保持宽高比例。

  4. 计算填充信息:

    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    

    计算缩放后图像的尺寸,并计算需要填充的宽度和高度。

  5. 自适应最小矩形:

    if auto:  # minimum rectangledw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    

    如果启用了auto选项,确保填充的宽度和高度是stride的整数倍,以保持最小矩形。

  6. 分配填充到两侧:

    dw /= 2  # divide padding into 2 sides
    dh /= 2
    

    将填充均匀分配到图像的两侧。

  7. 调整图像大小:

    if shape[::-1] != new_unpad:  # resizeim = cv.resize(im, new_unpad, interpolation=cv.INTER_LINEAR)
    

    如果缩放后的图像尺寸与目标尺寸不同,使用线性插值调整图像大小。

  8. 边框填充:

    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv.copyMakeBorder(im, top, bottom, left, right, cv.BORDER_CONSTANT, value=color)  # add border
    

    使用OpenCV的copyMakeBorder函数,以指定的颜色填充图像的边框。

  9. 返回结果:

    return im, ratio, (dw, dh)
    

    返回处理后的图像、缩放比例和填充信息。

需要注意的是:

a.这里填充的是黑色,即(0,0,0),而Yolov5中填充的是灰色,即(114,114,114),都是一样的效果。

b.训练时没有采用缩减黑边的方式,还是采用传统填充的方式,即缩放到416*416大小。只是在测试,使用模型推理时,才采用缩减黑边的方式,提高目标检测,推理的速度。

c.为什么np.mod函数的后面用32?因为Yolov5的网络经过5次下采样,而2的5次方,等于32。所以至少要去掉32的倍数,再进行取余。


这种方式在之前github上Yolov3中也进行了讨论:https://github.com/ultralytics/yolov3/issues/232

在讨论中,通过这种简单的改进,推理速度得到了37%的提升,可以说效果很明显。

在这里插入图片描述

Focus结构

资料参考:https://blog.csdn.net/qq_39056987/article/details/112712817
Focus模块是在V5版本中的一个图像处理模块,其主要作用是在图像输入到卷积神经网络的backbone之前进行切片操作。该模块通过在一张图片中每隔一个像素提取一个值,实现类似邻近下采样的效果,从而生成四张互补且大小相近的图片。这一操作将宽度(W)和高度(H)的信息集中到了通道空间。因此,输入通道数扩展了4倍,即将这四张图片拼接在一起,使得相对于原始的RGB三通道模式,通道数增至12。最后,经过卷积操作,得到没有信息丢失的二倍下采样特征图。
在这里插入图片描述
代码实现:

class Focus(nn.Module):# Focus wh information into c-spacedef __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groupssuper(Focus, self).__init__()self.conv = Conv(c1 * 4, c2, k, s, p, g, act)      # 这里输入通道变成了4倍def forward(self, x):  # x(b,c,w,h) -> y(b,4c,w/2,h/2)return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

YOLOv5s 6.0版本之后,然而又改回卷积了。

CSP结构 Neck

Yolov5现在的Neck和Yolov4中一样,都采用FPN+PAN的结构,但在Yolov5刚出来时,只使用了FPN结构,后面才增加了PAN结构,此外网络中其他部分也进行了调整。

Yolov4的Neck结构中,采用的都是普通的卷积操作。而Yolov5的Neck结构中,采用借鉴CSPnet设计的CSP2结构,加强网络特征融合的能力。

Yolov4网络结构中,借鉴了CSPNet的设计思路,在主干网络中设计了CSP结构。

Yolov5与Yolov4不同点在于,Yolov4中只有主干网络使用了CSP结构。

而Yolov5中设计了两种CSP结构,以Yolov5s网络为例,CSP1_X结构应用于Backbone主干网络,另一种CSP2_X结构则应用于Neck中。

在这里插入图片描述
图片出处:https://zhuanlan.zhihu.com/p/172121380

CSPNet全称是Cross Stage Paritial Network,主要从网络结构设计的角度解决推理中从计算量很大的问题。

CSPNet的作者认为推理计算过高的问题是由于网络优化中的梯度信息重复导致的。

因此采用CSP模块先将基础层的特征映射划分为两部分,然后通过跨阶段层次结构将它们合并,在减少了计算量的同时可以保证准确率。

CSPNet论文地址:https://arxiv.org/pdf/1911.11929.pdf
在这里插入图片描述
左为原始的Dense Block,右者为Partial Dense Block。假设将输入按照part_ratio=0.5的比例分成两部分,并且假设一个Dense Block的输入为w * h * c,growth rate为 d,Dense block的layer数量为m,

CIO (Convolutional input/Output)
对于原始的DenseBlockCIO = cm + [(mm+m)d]/2
而对于使用了CSP结构的结构来说,CIO = [cm + (mm+m)d]/2

代码实现:

class DenseBlock(nn.Module):def __init__(self, input_channels, num_layers, growth_rate):super(DenseBlock, self).__init__()self.num_layers = num_layersself.k0 = input_channelsself.k = growth_rateself.layers = self.__make_layers()def __make_layers(self):layer_list = []for i in range(self.num_layers):layer_list.append(nn.Sequential(BN_Conv2d(self.k0 + i * self.k, 4 * self.k, 1, 1, 0),BN_Conv2d(4 * self.k, self.k, 3, 1, 1)))return layer_listdef forward(self, x):feature = self.layers[0](x)out = torch.cat((x, feature), 1)for i in range(1, len(self.layers)):feature = self.layers[i](out)out = torch.cat((feature, out), 1)return outclass CSP_DenseBlock(nn.Module):def __init__(self, in_channels, num_layers, k, part_ratio=0.5):super(CSP_DenseBlock, self).__init__()self.part1_chnls = int(in_channels * part_ratio)self.part2_chnls = in_channels - self.part1_chnlsself.dense = DenseBlock(self.part2_chnls, num_layers, k)def forward(self, x):part1 = x[:, :self.part1_chnls, :, :]part2 = x[:, self.part1_chnls:, :, :]part2 = self.dense(part2)out = torch.cat((part1, part2), 1)return out

DenseBlock 类:

  • __init__:使用指定的参数(input_channels,输入通道维度,num_layers,密集块中的层数,以及growth_rate,通道数量的增长率)初始化DenseBlock模块。

  • __make_layers:创建密集块层的私有方法。它通过指定数量的层(num_layers)迭代,并将一个由自定义BN_Conv2d模块构成的卷积层对附加到layer_list。第一卷积层的输出通道大小为4 * self.k(其中self.k是增长率),第二卷积层的输出通道大小为self.k

  • forward:定义DenseBlock的前向传播。它通过层进行迭代,将输入张量与每个层的输出连接起来。结果作为密集块的输出返回。

CSP_DenseBlock 类:

  • __init__:使用指定的参数(in_channels,输入通道维度,num_layers,密集块中的层数,k,通道数量的增长率,以及可选的part_ratio参数,默认值为0.5,表示第一部分中通道的比例)初始化CSP_DenseBlock模块。

  • forward:定义CSP_DenseBlock的前向传播。它根据指定的通道比率将输入张量分割成两部分(part1part2)。然后,通过DenseBlock模块处理第二部分(part2)。最后,通过连接part1DenseBlock的输出获得输出。

CIOU_Loss

Yolov5中采用其中的CIOU_Loss做Bounding box的损失函数。

Yolov4中也采用CIOU_Loss作为目标Bounding box的损失。

下面介绍一下IOU loss的发展过程:
Intersection over Union (IoU)是用于评估目标检测模型性能的常见指标之一。IoU是通过计算预测框和真实目标框之间的交集面积与它们的并集面积的比例来衡量的。具体而言,IoU的计算公式如下:

I o U = Intersection Area Union Area IoU = \frac{\text{Intersection Area}}{\text{Union Area}} IoU=Union AreaIntersection Area

GIoU:
在这里插入图片描述
GIoU参考资料:
https://giou.stanford.edu/
https://sh-tsang.medium.com/review-giou-generalized-intersection-over-union-b4dd1ab89493

在对象检测中,IoU被用作评估预测边界框与地面实况的接近程度的度量。在上面的第一个例子中,预测和地面实况边界框重叠,因此IoU的值是非零的。让我们看一个IoU不足的例子。

当预测框和目标框不相交时,它们的交集面积为零。这意味着分母中的并集面积就是两者的面积之和。因此,IoU的计算将变为零除以非零的值,结果将始终为零。

设(A)为预测框的区域,(B)为目标框的区域。如果 ( A ∩ B = ∅ ) (A \cap B = \emptyset) (AB=)交集为空,那么 I o U = 0 A ∪ B = 0 IoU = \frac{0}{A \cup B} = 0 IoU=AB0=0在这种情况下,IoU的值为零,说明两个框不相交。

如果我们进行了一个糟糕的预测,其中预测的边界框与基本事实没有重叠,该怎么办。在这种情况下,以及在地面实况和预测边界框之间没有重叠的任何其他情况下,交集为0,因此IoU也将为0。如果IoU即使在没有交集的情况下,我们新的、更好的预测也比第一个预测更接近地面实况,那就太好了。

《Generalized Intersection over Union: A Metric and A Loss for Bounding Box Regression》
于是提出了一个解决方案,GIoU,其公式如下:
G I o U = ∣ A ∩ B ∣ ∣ A ∪ B ∣ − ∣ C \ ( A ∪ B ) ∣ ∣ C ∣ = I o U − ∣ C \ ( A ∪ B ) ∣ ∣ C ∣ \begin{gathered}GIoU=\frac{|A\cap B|}{|A\cup B|}-\frac{|C\backslash(A\cup B)|}{|C|}=IoU\\-\frac{|C\backslash(A\cup B)|}{|C|}\end{gathered} GIoU=ABABCC\(AB)=IoUCC\(AB)

其中A和B是预测和实际边界框。C是包含A和B的最小凸包。

我们首先找到了包围A和B的最小凸包C 。然后,我们计算C(不包括A和B)所占的体积(面积)除以C所占的总体积(面积)。在更好预测的情况下,C的面积会更小,所有其他值保持不变。IoU都将为0。因此,随着预测向地面实况移动,减去较小的值,GIoU的值增加

在一个实数向量空间V中,对于给定集合X所有包含X的凸集的交集S为X的凸包。
在这里插入图片描述

在点集拓扑学与欧几里得空间中,凸集(Convex set)是一个点集合,其中每两点之间的线段点都落在该点集合中。
在这里插入图片描述在这里插入图片描述

随着预测向地面实况移动,减去较小的值,GIoU的值增加
在这里插入图片描述在这里插入图片描述

回想一下,在神经网络中,任何给定的损失函数都必须是可微分的,才能允许反向传播。我们从上面的例子中看到,在没有交集的情况下,IoU没有值,因此没有梯度。然而,GIoU总是可微的。
我们对预测边界框与地面实况重叠(即相交)的情况和没有相交的情况进行了采样。这些样本的IoU和GIoU之间的关系如图所示。
从图中可以看出,与上述公式一样,GIoU的范围从-1到1。当包围两个边界框的面积(例如C)大于IoU时,会出现负值。随着IoU分量的增加,GIoU的值收敛到IoU。
在这里插入图片描述
然而GIoU有一个问题,一旦预测框和目标框出现包含关系,或者宽和高对齐的情况,差集为0,GIoU就退化成了IoU,无法评估相对位置,收敛缓慢。
在这里插入图片描述
我们可以从图中观察到,图3即使在250次迭代后,预测框(蓝色)也会扩展到与目标框(绿色)重叠,并且在预测框的中心坐标中只有很小的位移。图4中的垂直拉伸也是如此。因此,尤其是对于水平和垂直方向的盒子GIoU损失收敛缓慢。

DIoU:

Distance-IoU:https://arxiv.org/abs/1911.08287
参考资料:https://medium.com/visionwizard/understanding-diou-loss-a-quick-read-a4a0fbcbf0f0

DIoU(距离-并集上的交集)损失函数包含预测框和目标框之间的归一化距离。

它不仅继承了IoU和GIoU的属性,还解决了它们都表现不佳的问题:它直接最小化了两个盒子的距离,因此收敛速度比GIoU损失快得多,尤其是在非重叠的情况下。

它还考虑了垂直和水平方向(图3和图4)DIoU可以更快地解决回归问题的情况。

R ( B , B g t ) \mathcal{R}(B,B^{gt}) R(B,Bgt) 是预测框 B B B 和目标框 B g t B^{gt} Bgt的惩罚项。

通常基于IoU的损失所提出的方法可以定义为
L = 1 − I o U + R ( B , B g t ) , \mathcal{L}=1-IoU+\mathcal{R}(B,B^{gt}), L=1IoU+R(B,Bgt),

其中,b和b g t ^{gt} gt表示 b b b b g t b^{gt} bgt 的中心点, ρ ( ⋅ ) \rho(\cdot) ρ是欧几里得距离, c c c是覆盖这两个盒子的最小封闭盒子的对角线长度。然后DIoU损失函数可以定义为:

L D I o U = 1 − I o U + ρ 2 ( b , b g t ) c 2 \mathcal{L}_{DIoU}=1-IoU+\frac{\rho^{2}(\mathbf{b},\mathbf{b}^{gt})}{c^{2}} LDIoU=1IoU+c2ρ2(b,bgt)

在这里插入图片描述
DIoU损失的惩罚项直接使两个中心点之间的距离最小化。 c 的作用是防止损失函数的值过大,有点像标准化的感觉。

参考资料:https://zhuanlan.zhihu.com/p/374907266

在这里插入图片描述
最终迭代T时IoU、GIoU和DIoU损失的回归误差可视化,即每个坐标n的e(T,n)。我们注意到(a)和(b)中的盆地对应于良好的回归情况。可以看出,在非重叠情况下,IoU损失有很大的误差,在水平和垂直情况下,GIoU损失有较大的误差,而DIoU损失导致到处都有非常小的回归误差。

当与目标框不重叠时,DIoU可以为边界框提供移动方向。与GIoU相比,预测框显著地向目标边界框移动。
在这里插入图片描述

DIoU要比GIou更加符合目标框回归的机制,将目标与anchor之间的距离,重叠率以及尺度都考虑进去,使得目标框回归变得更加稳定,不会像IoU和GIoU一样出现训练过程中发散等问题。

损失相当于在保留GIoU损失优点的基础上,增加了中心点距离度量,直接优化两个框中心距离,快速收敛,而且仅中心点完全重合的时候,才会退化成IoU。

但从式 L D I o U \mathcal{L}_{DIoU} LDIoU中我们可以看出 L D I o U \mathcal{L}_{DIoU} LDIoU 和闭包的对角线距离 c c c 成反比,当两个bounding box的中心点之间的距离不变时,闭包的对角线越长,则DIoU损失函数的值越小,这就意味着DIoU Loss可能存在DIoU Loss存在训练过程中预测框被错误放大,但是损失值变小的问题。

两个没有互相覆盖的矩形框,它们都是边长为 w w w 的矩形,它们中心点的距离为 2 w 2w 2w 。中心点不变,但是边长扩大到了 2 w 2w 2w,两种情况下DIoU的值分别是 0.4 和 0.246 ,因此DloU Loss也存在预测框被错误放大的问题。


CIoU:

上面的论文考虑到bbox回归三要素中的长宽比还没被考虑到计算中,因此,进一步在DloU的基础上提出了CloU。其惩罚项如下面公式: R C I o U = ρ 2 ( b , b g t ) c 2 + α v {\cal R}_{CIoU}=\frac{\rho^{2}(\mathbf{b},\mathbf{b}^{gt})}{c^{2}}+\alpha v RCIoU=c2ρ2(b,bgt)+αv 其中 α \alpha α 是权重函数, 而 ν \nu ν 用来度量长宽比的相似性,定义为 v = 4 π 2 ( arctan ⁡ w g t h g t − arctan ⁡ w h ) 2 v=\frac4{\pi^2}\left(\arctan\frac{w^{gt}}{h^{gt}}-\arctan\frac wh\right)^2 v=π24(arctanhgtwgtarctanhw)2 完整的 CloU 损失函数定义: L C I o U = 1 − I o U + ρ 2 ( b , b g t ) c 2 + α v \mathcal{L}_{CIoU}=1-IoU+\frac{\rho^2\left(\mathbf{b},\mathbf{b}^{gt}\right)}{c^2}+\alpha v LCIoU=1IoU+c2ρ2(b,bgt)+αv 最后,CloU loss的梯度类似于DloU Ioss, 但还要考虑 ν \nu ν 的梯度。在长宽在 [0,1] 的情况下, w 2 + h 2 w^2+h^2 w2+h2 的值通常很小,会导致梯度爆炸,因此在 1 w 2 + h 2 \frac1{w^2+h^2} w2+h21 实现时将替换成1。

def bbox_overlaps_ciou(bboxes1, bboxes2):rows = bboxes1.shape[0]cols = bboxes2.shape[0]cious = torch.zeros((rows, cols))if rows * cols == 0:return ciousexchange = Falseif bboxes1.shape[0] > bboxes2.shape[0]:bboxes1, bboxes2 = bboxes2, bboxes1cious = torch.zeros((cols, rows))exchange = Truew1 = bboxes1[:, 2] - bboxes1[:, 0]h1 = bboxes1[:, 3] - bboxes1[:, 1]w2 = bboxes2[:, 2] - bboxes2[:, 0]h2 = bboxes2[:, 3] - bboxes2[:, 1]area1 = w1 * h1area2 = w2 * h2center_x1 = (bboxes1[:, 2] + bboxes1[:, 0]) / 2center_y1 = (bboxes1[:, 3] + bboxes1[:, 1]) / 2center_x2 = (bboxes2[:, 2] + bboxes2[:, 0]) / 2center_y2 = (bboxes2[:, 3] + bboxes2[:, 1]) / 2inter_max_xy = torch.min(bboxes1[:, 2:],bboxes2[:, 2:])inter_min_xy = torch.max(bboxes1[:, :2],bboxes2[:, :2])out_max_xy = torch.max(bboxes1[:, 2:],bboxes2[:, 2:])out_min_xy = torch.min(bboxes1[:, :2],bboxes2[:, :2])inter = torch.clamp((inter_max_xy - inter_min_xy), min=0)inter_area = inter[:, 0] * inter[:, 1]inter_diag = (center_x2 - center_x1)**2 + (center_y2 - center_y1)**2outer = torch.clamp((out_max_xy - out_min_xy), min=0)outer_diag = (outer[:, 0] ** 2) + (outer[:, 1] ** 2)union = area1+area2-inter_areau = (inter_diag) / outer_diagiou = inter_area / unionwith torch.no_grad():arctan = torch.atan(w2 / h2) - torch.atan(w1 / h1)v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(w2 / h2) - torch.atan(w1 / h1)), 2)S = 1 - ioualpha = v / (S + v)w_temp = 2 * w1ar = (8 / (math.pi ** 2)) * arctan * ((w1 - w_temp) * h1)cious = iou - (u + alpha * ar)cious = torch.clamp(cious,min=-1.0,max = 1.0)if exchange:cious = cious.Treturn cious
  1. 导入库:

    import torch
    from torch import math
    

    导入 PyTorch 库,并从 torch 中导入 math 模块。

  2. 函数定义:

    def bbox_overlaps_ciou(bboxes1, bboxes2):
    

    定义了一个名为 bbox_overlaps_ciou 的函数,该函数计算两组边界框之间的 CIoU。

  3. 初始化变量:

    rows = bboxes1.shape[0]
    cols = bboxes2.shape[0]
    cious = torch.zeros((rows, cols))
    

    初始化行数、列数和 CIoU 矩阵。如果其中一组边界框为空,则直接返回全零的 CIoU 矩阵。

  4. 交换矩阵:

    exchange = False
    if bboxes1.shape[0] > bboxes2.shape[0]:bboxes1, bboxes2 = bboxes2, bboxes1cious = torch.zeros((cols, rows))exchange = True
    

    如果第一组边界框的行数大于第二组,进行交换,确保第一组边界框的行数更少,同时初始化 CIoU 矩阵。

  5. 提取边界框参数:

    w1 = bboxes1[:, 2] - bboxes1[:, 0]
    h1 = bboxes1[:, 3] - bboxes1[:, 1]
    w2 = bboxes2[:, 2] - bboxes2[:, 0]
    h2 = bboxes2[:, 3] - bboxes2[:, 1]
    

    提取边界框的宽度和高度。

  6. 计算面积和中心点:

    area1 = w1 * h1
    area2 = w2 * h2center_x1 = (bboxes1[:, 2] + bboxes1[:, 0]) / 2
    center_y1 = (bboxes1[:, 3] + bboxes1[:, 1]) / 2
    center_x2 = (bboxes2[:, 2] + bboxes2[:, 0]) / 2
    center_y2 = (bboxes2[:, 3] + bboxes2[:, 1]) / 2
    

    计算边界框的面积和中心点坐标。

  7. 计算交集和并集:

    inter_max_xy = torch.min(bboxes1[:, 2:], bboxes2[:, 2:])
    inter_min_xy = torch.max(bboxes1[:, :2], bboxes2[:, :2])
    out_max_xy = torch.max(bboxes1[:, 2:], bboxes2[:, 2:])
    out_min_xy = torch.min(bboxes1[:, :2], bboxes2[:, :2])inter = torch.clamp((inter_max_xy - inter_min_xy), min=0)
    inter_area = inter[:, 0] * inter[:, 1]
    

    计算边界框的交集和并集的坐标和面积。

  8. 计算对角线和 Complete IoU:

    inter_diag = (center_x2 - center_x1) ** 2 + (center_y2 - center_y1) ** 2
    outer = torch.clamp((out_max_xy - out_min_xy), min=0)
    outer_diag = (outer[:, 0] ** 2) + (outer[:, 1] ** 2)
    union = area1 + area2 - inter_area
    u = (inter_diag) / outer_diag
    iou = inter_area / union
    

    计算对角线、外接框和 Complete IoU。

  9. 计算 CIoU 中的其他参数:

    with torch.no_grad():arctan = torch.atan(w2 / h2) - torch.atan(w1 / h1)v = (4 / (math.pi ** 2)) * torch.pow((torch.atan(w2 / h2) - torch.atan(w1 / h1)), 2)S = 1 - ioualpha = v / (S + v)w_temp = 2 * w1ar = (8 / (math.pi ** 2)) * arctan * ((w1 - w_temp) * h1)cious = iou - (u + alpha * ar)cious = torch.clamp(cious, min=-1.0, max=1.0)
    

    计算 CIoU 中的额外参数,然后使用这些参数
    计算 CIoU,最后将 CIoU 的值夹在 [-1.0, 1.0] 的范围内。

  10. 矩阵交换:

    if exchange:cious = cious.T
    

    如果发生了交换,将 CIoU 矩阵转置回原始形状。

  11. 返回结果:

    return cious
    

    返回计算得到的 CIoU 矩阵。

nms非极大值抑制

在目标检测的后处理过程中,针对很多目标框的筛选,通常需要nms操作。

因为CIOU_Loss中包含影响因子v,涉及groudtruth的信息,而测试推理时,是没有groundtruth的。

所以Yolov4在DIOU_Loss的基础上采用DIOU_nms的方式,而Yolov5中采用加权nms的方式。

可以看出,采用DIOU_nms,下方中间箭头的黄色部分,原本被遮挡的摩托车也可以检出。

将nms中IOU修改成DIOU_nms。对于一些遮挡重叠的目标,会有一些改进。
在这里插入图片描述
图片:https://zhuanlan.zhihu.com/p/172121380

代码

Yolov5的作者并没有发表论文,因此只能从代码角度进行分析。

Yolov5代码:https://github.com/ultralytics/yolov5

大家可以根据网页的说明,下载训练,及测试。

最后

综合而言,在实际测试中,Yolov4的准确性有不错的优势,但Yolov5的多种网络结构使用起来更加灵活,我们可以根据不同的项目需求,取长补短,发挥不同检测网络的优势。

Yolox的改进trick中,有很多值得借鉴的地方。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java 集合】ConcurrentHashMap (JDK 1.8 版本)

1 ConcurrentHashMap 简介 Map 一种存储键值对 (key-value) 的数据结构, 可以通过 key 快速地定位到需要的 value, 在 Java 中是一个使用频率很高的一个数据结构。一般情况下, 我们都是可以直接使用它的实现类 HashMap 就能满足需求了。 但是 HashMap 在多线程情况, 并不是一个…

截断霍夫曼编码

截断霍夫曼编码是一种数据压缩技术&#xff0c;它基于霍夫曼编码的原理&#xff0c;通过截断霍夫曼树&#xff0c;减少编码中的冗余信息&#xff0c;实现更高效的数据压缩。在本文中&#xff0c;我们将详细探讨截断霍夫曼编码的原理、应用及其优势。 一、霍夫曼编码简介 霍夫曼…

真一键关闭BitLocker!

网管小贾 / sysadm.cc 同事老莫近日喜提新电脑一台&#xff0c;遂请我周末去他家帮忙给电脑开开光。 我口送佛号欣然应允&#xff0c;心中暗道又能喝到嫂夫人的私人定制绝美养生鸡汤&#xff0c;嘿嘿&#xff0c;阿弥陀佛&#xff0c;善哉善哉&#xff01; 老莫家就租住在市中…

Actuator内存泄露及利用Swagger未授权自动化测试实现

目录 0x00 前言 0x01 Actuator 泄露及利用 1、Actuator heapdump 内存泄露 2、知道泄露后如何进一步利用 3、如何发现 Actuator 泄露&#xff08;白盒/黑盒&#xff09; 0x02 Swagger自动化测试 1、什么是Swagger&#xff1f; 2、PostmanBurpSuiteXray 联动 3、思考 0x…

腾讯云debian服务器的连接与初始化

目录 1. 远程连接2. 软件下载3. 设置开机自启动 1. 远程连接 腾讯云给的服务器在安装好系统之后&#xff0c;只需要在防火墙里面添加一个白名单&#xff08;ip 或者域名&#xff09;就能访问了。 浏览器打开https://www.ipip.net/&#xff0c;在左下角找到自己所用的WIFI的公…

java使用面向对象实现图书管理系统

꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN …

MFC静态链接+libtiff静态链接提示LNK2005和LNK4098

编译报错 1>msvcrt.lib(ti_inst.obj) : error LNK2005: "private: __thiscall type_info::type_info(class type_info const &)" (??0type_infoAAEABV0Z) 已经在 libcmtd.lib(typinfo.obj) 中定义 1>msvcrt.lib(ti_inst.obj) : error LNK2005: "pr…

Jenkins Docker Cloud在Linux应用开发CI中的实践

Jenkins Docker Cloud在Linux应用开发CI中的实践 背景 通过代码提交自动触发CI自动构建、编译、打包是任何软件开发组织必不可少的基建&#xff0c;可以最大程度保证产物的一致性&#xff0c;方便跨组跨部门协作&#xff0c;代码MR等。 Docker在流水线中越来越重要&#xff…

解决Maven找不到依赖的问题

如果经过Reload Maven项目&#xff0c;清除Idea缓存&#xff0c;甚至重启Idea等方法都解决不了Dependency xxx not found的问题&#xff0c;不妨试试手动安装。 1. 进入maven仓库&#xff0c;搜索自己需要的对应版本的依赖。 2. 点击下图红框jar图标下载对应的jar包&#xff0c…

[德人合科技]——设计公司 \ 设计院图纸文件数据 | 资料透明加密防泄密软件

国内众多设计院都在推进信息化建设&#xff0c;特别是在异地办公、应用软件资产规模、三维设计技术推广应用以及协同办公等领域&#xff0c;这些加快了业务的发展&#xff0c;也带来了更多信息安全挑战&#xff0c;尤其是对于以知识成果为重要效益来源的设计院所&#xff0c;防…

MyBatis-Plus如何 关闭SQL日志打印

前段时间公司的同事都过来问我&#xff0c;hua哥公司的项目出问题了&#xff0c;关闭不了打印sql日记&#xff0c;项目用宝塔自己部署的&#xff0c;磁盘满了才发现大量的打印sql日记&#xff0c;他们百度过都按照网上的配置修改过不起作用&#xff0c;而且在调试时候也及为不方…

软件设计模式:六大设计原则

文章目录 前言一、开闭原则二、里氏替换原则三、依赖倒转原则四、接口隔离五、迪米特法则六、合成复用原则总结 前言 在软件开发中&#xff0c;为了提高软件系统的可维护性和可复用性&#xff0c;增加软件的可扩展性和灵活性&#xff0c;程序员要尽量根据6条原则来开发程序&am…

STM32_窗口看门狗

什么是窗口看门狗&#xff1f; 窗口看门狗用于监测单片机程序运行时效是否精准&#xff0c;主要检测软件异常&#xff0c;一般用于需要精准检测 程序运行时间的场合。 窗口看门狗的本质是一个能产生 系统复位信号 和 提前唤醒中断 的 6 位计数器 产生复位条件&#xff1a; 当…

数组循环左移

数组循环左移是指将数组的元素向左移动一定的位置&#xff0c;使得数组的最后一个元素移动到数组的第一个位置&#xff0c;数组的倒数第二个元素移动到数组的第二个位置&#xff0c;以此类推。 以下是一个示例代码&#xff0c;演示如何实现数组循环左移&#xff1a; def rota…

力扣题目学习笔记(OC + Swift) 14. 最长公共前缀

14. 最长公共前缀 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 “”。 方法一 竖向扫描法 个人感觉纵向扫描方式比较直观&#xff0c;符合人类理解方式&#xff0c;从前往后遍历所有字符串的每一列&#xff0c;比较相同列上的…

服务器解析漏洞是什么?攻击检测及修复

服务器解析漏洞&#xff08;Server-side Include Vulnerability&#xff0c;SSI漏洞&#xff09;是一种安全漏洞&#xff0c;通常出现在支持服务器端包含&#xff08;SSI&#xff09;功能的Web服务器上。SSI是一种在Web页面中嵌入动态内容的技术&#xff0c;允许开发人员将外部…

Gradle中 Implementation 与API 声明依赖方式的对比

在Gradle中&#xff0c;implementation和api是声明依赖的两种方式&#xff0c;它们在如何暴露依赖关系方面有所不同&#xff1a; Implementation: 当一个模块使用implementation声明依赖时&#xff0c;该依赖仅对声明它的模块可见。这意味着该依赖对于该模块的消费者是隐藏的。…

Android 架构 - 组件化

一、概念 组件化是对单个功能进行开发&#xff0c;使得功能可以复用。将多个功能组合起来就是一个业务模块&#xff0c;因此去除了模块间的耦合&#xff0c;使得按业务划分的模块成了可单独运行的业务组件。&#xff08;一定程度上的独立&#xff0c;还是依附于整个项目中&…

如何使用支付宝的沙箱环境在本地配置模拟支付并发布至公网测试

文章目录 前言1. 下载当面付demo2. 修改配置文件3. 打包成web服务4. 局域网测试5. 内网穿透6. 测试公网访问7. 配置二级子域名8. 测试使用固定二级子域名访问 前言 在沙箱环境调试支付SDK的时候&#xff0c;往往沙箱环境部署在本地&#xff0c;局限性大&#xff0c;在沙箱环境…

vue关闭当前路由页面并跳转到其父页面

1.dom中添加关闭或取消按钮 <el-button type"primary" class"blueLinearbg cancelBtn" click"cancel" >取 消</el-button>2.cancel方法中 /*取消或关闭*/cancel(){this.$store.dispatch("tagsView/delView", this.$route)…