济南好的网站建设公司免费seo网站

bicheng/2026/1/22 12:57:38/文章来源:
济南好的网站建设公司,免费seo网站,资兴网站建设,wap网文章目录 0 简介1 课题背景#x1f6a9; 2 口罩佩戴算法实现2.1 YOLO 模型概览2.2 YOLOv32.3 YOLO 口罩佩戴检测实现数据集 2.4 实现代码2.5 检测效果 3 口罩佩戴检测算法评价指标3.1 准确率#xff08;Accuracy#xff09;3.2 精确率(Precision)和召回率(Recall)3.3 平均精… 文章目录 0 简介1 课题背景 2 口罩佩戴算法实现2.1 YOLO 模型概览2.2 YOLOv32.3 YOLO 口罩佩戴检测实现数据集 2.4 实现代码2.5 检测效果 3 口罩佩戴检测算法评价指标3.1 准确率Accuracy3.2 精确率(Precision)和召回率(Recall)3.3 平均精度(Average precision AP) 4 最后 0 简介 优质竞赛项目系列今天要分享的是 基于深度学习的口罩佩戴检测【全网最详细】 - opencv 卷积神经网络 机器视觉 深度学习 该项目较为新颖适合作为竞赛课题方向学长非常推荐 更多资料, 项目分享 https://gitee.com/dancheng-senior/postgraduate 1 课题背景 从2019年末开始新型冠状病毒肺炎COVID-19在我国全面爆发并迅速传播同时国家卫生健康委员会也积极响应密切关注全国疫情的动态变化并且发布了相关的预防指南强调个人出行需要做好安全措施在公共场合必须严格按照要求佩戴口罩。自从新型冠状病毒蔓延以来各行各业都受到了巨大的冲击严重影响到人们正常生产和生活。 新型冠状病毒具有很强的传播和生存能力只要条件合适能存活五天之久并且可以通过唾液飞沫等多种方式进行传播为有效的减少病毒的传播佩戴口罩是一个很好的办法。尽管这一时期国外的形势不容乐观但是在全国上下齐心努力之下我国的防疫取得了阶段性成功各行业都在积极复苏管理也随之变化进入到常态化阶段。在这一阶段复工复产也是大势所趋口罩出行也成为了一种常态。正确佩戴口罩能够有效减少飞沫传染的风险特别是在公共场所这种举措尤为重要。但是仍然还需要提高公众对主动佩戴口罩的观念在常态化管理下人们的防范意识越来越薄弱口罩随意佩戴或者不佩戴的情况屡见不鲜。 因此在这期间有意识地戴口罩不仅仅是每个公民的公共道德还是自我修养的表现。这不但需要个人积极配合而且还需要某些监管以及有效的治理方法。 2 口罩佩戴算法实现 2.1 YOLO 模型概览 YOLO 的缩写是 You only look once。YOLO 模型可以直接根据图片输出包含对象的区域与区域对应的分类一步到位不像 RCNN 系列的模型需要先计算包含对象的区域再根据区域判断对应的分类YOLO 模型的速度比 RCNN 系列的模型要快很多。 YOLO 模型的结构如下 是不是觉得有点熟悉看上去就像 Faster-RCNN 的区域生成网络 (RPN) 啊。的确YOLO 模型原理上就是寻找区域的同时判断区域包含的对象分类YOLO 模型与区域生成网络有以下的不同 YOLO 模型会输出各个区域是否包含对象中心而不是包含对象的一部分YOLO 模型会同时输出对象分类YOLO 模型输出的区域偏移会根据对象中心点计算具体算法在下面说明 YOLO 模型与 Faster-RCNN 的区域生成网络最大的不同是会判断各个区域是否包含对象中心如下图中狗脸覆盖了四个区域但只有左下角的区域包含了狗脸的中心YOLO 模型应该只判断这个区域包含对象。 当然如果对象中心非常接近区域的边界那么判断起来将会很困难YOLO 模型在训练的时候会忽略对象重叠率高于一定水平的区域具体可以参考后面给出的代码。 YOLO 模型会针对各个区域输出以下的结果这里假设有三个分类 是否包含对象中心 (是为 1, 否为 0)区域偏移 x区域偏移 y区域偏移 w区域偏移 h分类 1 的可能性 (0 ~ 1)分类 2 的可能性 (0 ~ 1)分类 3 的可能性 (0 ~ 1) 输出结果的维度是 批次大小, 区域数量, 5 分类数量。 区域偏移用于调整输出的区域范围例如上图中狗脸的中心点大约在区域的右上角如果把区域左上角看作 (0, 0)右下角看作 (1, 1)那么狗脸中心点应该在 (0.95, 0.1) 的位置而狗脸大小相对于区域长宽大概是 (1.3, 1.5) 倍生成训练数据的时候会根据这 4 个值计算区域偏移具体计算代码在下面给出。 看到这里你可能会想YOLO 模型看起来很简单啊我可以丢掉操蛋的 Faster-RCNN 模型了。 不没那么简单以上介绍的只是 YOLOv1 模型YOLOv1 模型的精度非常低后面为了改进识别精度还发展出 YOLOv2, YOLOv3, YOLOv4, YOLOv5 模型学长下面会给出 YOLOv3 模型的实现。Y OLOv4 和 YOLOv5 模型主要改进了提取特征用的 CNN 模型 (也称骨干网络 Backbone Network)原始的 YOLO 模型使用了 C 语言编写的 Darknet 作为骨干网络而这篇使用 Resnet 作为骨干网络所以今天学长只介绍到 YOLOv3。 2.2 YOLOv3 YOLOv3 引入了多尺度检测机制 (Multi-Scale Detection)这个机制可以说是 YOLO 模型的精华引入这个机制之前 YOLO 模型的精度很不理想而引入之后 YOLO 模型达到了接近 Faster-RCNN 的精度并且速度还是比 Faster-RCNN 要快。 多尺度检测机制简单的来说就是按不同的尺度划分区域然后再检测这些不同大小的区域是否包含对象检测的时候大区域的特征会混合到小区域中使得小区域判断时拥有一定程度的上下文信息。 2.3 YOLO 口罩佩戴检测实现 接下来学长带大家用 YOLO 模型把没带口罩的家伙抓出来吧。 数据集 学长的这个数据集包含了 8535 张图片 (部分图片没有使用)其中各个分类的数量如下 戴口罩的区域 (with_mask): 3232 个不戴口罩的区域 (without_mask): 717 个带了口罩但姿势不正确的区域 (mask_weared_incorrect): 123 个 因为带了口罩但姿势不正确的样本数量很少所以都归到戴口罩里面去。 2.4 实现代码 使用这个数据集训练并且训练成功以后使用模型识别图片或视频的完整代码如下 ​ import os import sys import torch import gzip import itertools import random import numpy import math import pandas import json from PIL import Image from PIL import ImageDraw from PIL import ImageFont from torch import nn from matplotlib import pyplot from collections import defaultdict from collections import deque import xml.etree.cElementTree as ET# 缩放图片的大小 IMAGE_SIZE (256, 192) # 训练使用的数据集路径 DATASET_1_IMAGE_DIR ./archive/images DATASET_1_ANNOTATION_DIR ./archive/annotations DATASET_2_IMAGE_DIR ./784145_1347673_bundle_archive/train/image_data DATASET_2_BOX_CSV_PATH ./784145_1347673_bundle_archive/train/bbox_train.csv # 分类列表 # YOLO 原则上不需要 other 分类但实测中添加这个分类有助于提升标签分类的精确度 CLASSES [ other, with_mask, without_mask ] CLASSES_MAPPING { c: index for index, c in enumerate(CLASSES) } # 判断是否存在对象使用的区域重叠率的阈值 (另外要求对象中心在区域内) IOU_POSITIVE_THRESHOLD 0.30 IOU_NEGATIVE_THRESHOLD 0.30# 用于启用 GPU 支持 device torch.device(cuda if torch.cuda.is_available() else cpu)class BasicBlock(nn.Module):ResNet 使用的基础块expansion 1 # 定义这个块的实际出通道是 channels_out 的几倍这里的实现固定是一倍def __init__(self, channels_in, channels_out, stride):super().__init__()# 生成 3x3 的卷积层# 处理间隔 stride 1 时输出的长宽会等于输入的长宽例如 (32-32)//11 32# 处理间隔 stride 2 时输出的长宽会等于输入的长宽的一半例如 (32-32)//21 16# 此外 resnet 的 3x3 卷积层不使用偏移值 biasself.conv1 nn.Sequential(nn.Conv2d(channels_in, channels_out, kernel_size3, stridestride, padding1, biasFalse),nn.BatchNorm2d(channels_out))# 再定义一个让输出和输入维度相同的 3x3 卷积层self.conv2 nn.Sequential(nn.Conv2d(channels_out, channels_out, kernel_size3, stride1, padding1, biasFalse),nn.BatchNorm2d(channels_out))# 让原始输入和输出相加的时候需要维度一致如果维度不一致则需要整合self.identity nn.Sequential()if stride ! 1 or channels_in ! channels_out * self.expansion:self.identity nn.Sequential(nn.Conv2d(channels_in, channels_out * self.expansion, kernel_size1, stridestride, biasFalse),nn.BatchNorm2d(channels_out * self.expansion))def forward(self, x):# x conv1 relu conv2 relu# | ^# ||tmp self.conv1(x)tmp nn.functional.relu(tmp, inplaceTrue)tmp self.conv2(tmp)tmp self.identity(x)y nn.functional.relu(tmp, inplaceTrue)return yclass MyModel(nn.Module):YOLO (基于 ResNet 的变种)Anchors None # 锚点列表包含 锚点数量 * 形状数量 的范围AnchorSpans (16, 32, 64) # 尺度列表值为锚点之间的距离AnchorAspects ((1, 1), (1.5, 1.5)) # 锚点对应区域的长宽比例列表AnchorOutputs 1 4 len(CLASSES) # 每个锚点范围对应的输出数量是否对象中心 (1) 区域偏移 (4) 分类数量AnchorTotalOutputs AnchorOutputs * len(AnchorAspects) # 每个锚点对应的输出数量ObjScoreThreshold 0.9 # 认为是对象中心所需要的最小分数IOUMergeThreshold 0.3 # 判断是否应该合并重叠区域的重叠率阈值def __init__(self):super().__init__()# 抽取图片特征的 ResNet# 因为锚点距离有三个这里最后会输出各个锚点距离对应的特征self.previous_channels_out 4self.resnet_models nn.ModuleList([nn.Sequential(nn.Conv2d(3, self.previous_channels_out, kernel_size3, stride1, padding1, biasFalse),nn.BatchNorm2d(self.previous_channels_out),nn.ReLU(inplaceTrue),self._make_layer(BasicBlock, channels_out16, num_blocks2, stride1),self._make_layer(BasicBlock, channels_out32, num_blocks2, stride2),self._make_layer(BasicBlock, channels_out64, num_blocks2, stride2),self._make_layer(BasicBlock, channels_out128, num_blocks2, stride2),self._make_layer(BasicBlock, channels_out256, num_blocks2, stride2)),self._make_layer(BasicBlock, channels_out256, num_blocks2, stride2),self._make_layer(BasicBlock, channels_out256, num_blocks2, stride2)])# 根据各个锚点距离对应的特征预测输出的卷积层# 大的锚点距离抽取的特征会合并到小的锚点距离抽取的特征# 这里的三个子模型意义分别是:# - 计算用于合并的特征# - 放大特征# - 计算最终的预测输出self.yolo_detectors nn.ModuleList([nn.ModuleList([nn.Sequential(nn.Conv2d(256 if index 0 else 512, 256, kernel_size1, stride1, padding0, biasTrue),nn.ReLU(inplaceTrue),nn.Conv2d(256, 512, kernel_size3, stride1, padding1, biasTrue),nn.ReLU(inplaceTrue),nn.Conv2d(512, 256, kernel_size1, stride1, padding0, biasTrue),nn.ReLU(inplaceTrue)),nn.Upsample(scale_factor2, modenearest),nn.Sequential(nn.Conv2d(256, 512, kernel_size3, stride1, padding1, biasTrue),nn.ReLU(inplaceTrue),nn.Conv2d(512, 256, kernel_size3, stride1, padding1, biasTrue),nn.ReLU(inplaceTrue),nn.Conv2d(256, MyModel.AnchorTotalOutputs, kernel_size1, stride1, padding0, biasTrue))])for index in range(len(self.resnet_models))])# 处理结果范围的函数self.sigmoid nn.Sigmoid()def _make_layer(self, block_type, channels_out, num_blocks, stride):创建 resnet 使用的层blocks []# 添加第一个块blocks.append(block_type(self.previous_channels_out, channels_out, stride))self.previous_channels_out channels_out * block_type.expansion# 添加剩余的块剩余的块固定处理间隔为 1不会改变长宽for _ in range(num_blocks-1):blocks.append(block_type(self.previous_channels_out, self.previous_channels_out, 1))self.previous_channels_out * block_type.expansionreturn nn.Sequential(*blocks)staticmethoddef _generate_anchors():根据锚点和形状生成锚点范围列表w, h IMAGE_SIZEanchors []for span in MyModel.AnchorSpans:for x in range(0, w, span):for y in range(0, h, span):xcenter, ycenter x span / 2, y span / 2for ratio in MyModel.AnchorAspects:ww span * ratio[0]hh span * ratio[1]xx xcenter - ww / 2yy ycenter - hh / 2xx max(int(xx), 0)yy max(int(yy), 0)ww min(int(ww), w - xx)hh min(int(hh), h - yy)anchors.append((xx, yy, ww, hh))return anchorsdef forward(self, x):# 抽取各个锚点距离对应的特征# 维度分别是:# torch.Size([16, 256, 16, 12])# torch.Size([16, 256, 8, 6])# torch.Size([16, 256, 4, 3])features_list []resnet_input xfor m in self.resnet_models:resnet_input m(resnet_input)features_list.append(resnet_input)# 根据特征预测输出# 维度分别是:# torch.Size([16, 16, 4, 3])# torch.Size([16, 16, 8, 6])# torch.Size([16, 16, 16, 12])# 16 是 (5 分类3) * 形状2previous_upsampled_feature Noneoutputs []for index, feature in enumerate(reversed(features_list)):if previous_upsampled_feature is not None:# 合并大的锚点距离抽取的特征到小的锚点距离抽取的特征feature torch.cat((feature, previous_upsampled_feature), dim1)# 计算用于合并的特征hidden self.yolo_detectors[index][0](feature)# 放大特征 (用于下一次处理时合并)upsampled self.yolo_detectors[index][1](hidden)# 计算最终的预测输出output self.yolo_detectors[index][2](hidden)previous_upsampled_feature upsampledoutputs.append(output)# 连接所有输出# 注意顺序需要与 Anchors 一致outputs_flatten []for output in reversed(outputs):output output.permute(0, 2, 3, 1)output output.reshape(output.shape[0], -1, MyModel.AnchorOutputs)outputs_flatten.append(output)outputs_all torch.cat(outputs_flatten, dim1)# 是否对象中心应该在 0 ~ 1 之间使用 sigmoid 处理outputs_all[:,:,:1] self.sigmoid(outputs_all[:,:,:1])# 分类应该在 0 ~ 1 之间使用 sigmoid 处理outputs_all[:,:,5:] self.sigmoid(outputs_all[:,:,5:])return outputs_allstaticmethoddef loss_function(predicted, actual):YOLO 使用的多任务损失计算器result_tensor, result_isobject_masks, result_nonobject_masks actualobjectness_losses []offsets_losses []labels_losses []for x in range(result_tensor.shape[0]):mask_positive result_isobject_masks[x]mask_negative result_nonobject_masks[x]# 计算是否对象中心的损失分别针对正负样本计算# 因为大部分区域不包含对象中心这里减少负样本的损失对调整参数的影响objectness_loss_positive nn.functional.mse_loss(predicted[x,mask_positive,0], result_tensor[x,mask_positive,0])objectness_loss_negative nn.functional.mse_loss(predicted[x,mask_negative,0], result_tensor[x,mask_negative,0]) * 0.5objectness_losses.append(objectness_loss_positive)objectness_losses.append(objectness_loss_negative)# 计算区域偏移的损失只针对正样本计算offsets_loss nn.functional.mse_loss(predicted[x,mask_positive,1:5], result_tensor[x,mask_positive,1:5])offsets_losses.append(offsets_loss)# 计算标签分类的损失分别针对正负样本计算labels_loss_positive nn.functional.binary_cross_entropy(predicted[x,mask_positive,5:], result_tensor[x,mask_positive,5:])labels_loss_negative nn.functional.binary_cross_entropy(predicted[x,mask_negative,5:], result_tensor[x,mask_negative,5:]) * 0.5labels_losses.append(labels_loss_positive)labels_losses.append(labels_loss_negative)loss (torch.mean(torch.stack(objectness_losses)) torch.mean(torch.stack(offsets_losses)) torch.mean(torch.stack(labels_losses)))return lossstaticmethoddef calc_accuracy(actual, predicted):YOLO 使用的正确率计算器这里只计算是否对象中心与标签分类的正确率区域偏移不计算result_tensor, result_isobject_masks, result_nonobject_masks actual# 计算是否对象中心的正确率正样本和负样本的正确率分别计算再平均a result_tensor[:,:,0]p predicted[:,:,0] MyModel.ObjScoreThresholdobj_acc_positive ((a 1) (p 1)).sum().item() / ((a 1).sum().item() 0.00001)obj_acc_negative ((a 0) (p 0)).sum().item() / ((a 0).sum().item() 0.00001)obj_acc (obj_acc_positive obj_acc_negative) / 2# 计算标签分类的正确率cls_total 0cls_correct 0for x in range(result_tensor.shape[0]):mask list(sorted(result_isobject_masks[x] result_nonobject_masks[x]))actual_classes result_tensor[x,mask,5:].max(dim1).indicespredicted_classes predicted[x,mask,5:].max(dim1).indicescls_total len(mask)cls_correct (actual_classes predicted_classes).sum().item()cls_acc cls_correct / cls_totalreturn obj_acc, cls_accstaticmethoddef convert_predicted_result(predicted):转换预测结果到 (标签, 区域, 对象中心分数, 标签识别分数) 的列表重叠区域使用 NMS 算法合并# 记录重叠的结果区域, 结果是 [ [(标签, 区域, RPN 分数, 标签识别分数)], ... ]final_result []for anchor, tensor in zip(MyModel.Anchors, predicted):obj_score tensor[0].item()if obj_score MyModel.ObjScoreThreshold:# 要求对象中心分数超过一定值continueoffset tensor[1:5].tolist()offset[0] max(min(offset[0], 1), 0) # 中心点 x 的偏移应该在 0 ~ 1 之间offset[1] max(min(offset[1], 1), 0) # 中心点 y 的偏移应该在 0 ~ 1 之间box adjust_box_by_offset(anchor, offset)label_max tensor[5:].max(dim0)cls_score label_max.values.item()label label_max.indices.item()if label 0:# 跳过非对象分类continuefor index in range(len(final_result)):exists_results final_result[index]if any(calc_iou(box, r[1]) MyModel.IOUMergeThreshold for r in exists_results):exists_results.append((label, box, obj_score, cls_score))breakelse:final_result.append([(label, box, obj_score, cls_score)])# 合并重叠的结果区域 (使用 对象中心分数 * 标签识别分数 最高的区域为结果区域)for index in range(len(final_result)):exists_results final_result[index]exists_results.sort(keylambda r: r[2]*r[3])final_result[index] exists_results[-1]return final_resultstaticmethoddef fix_predicted_result_from_history(cls_result, history_results):根据历史结果减少预测结果中的误判适用于视频识别history_results 应为指定了 maxlen 的 deque# 要求历史结果中 50% 以上存在类似区域并且选取历史结果中最多的分类history_results.append(cls_result)final_result []if len(history_results) history_results.maxlen:# 历史结果不足不返回任何识别结果return final_resultfor label, box, rpn_score, cls_score in cls_result:# 查找历史中的近似区域similar_results []for history_result in history_results:history_result [(calc_iou(r[1], box), r) for r in history_result]history_result.sort(key lambda r: r[0])if history_result and history_result[-1][0] MyModel.IOUMergeThreshold:similar_results.append(history_result[-1][1])# 判断近似区域数量是否过半if len(similar_results) history_results.maxlen // 2:continue# 选取历史结果中最多的分类cls_groups defaultdict(lambda: [])for r in similar_results:cls_groups[r[0]].append(r)most_common sorted(cls_groups.values(), keylen)[-1]# 添加最多的分类中的最新的结果final_result.append(most_common[-1])return final_resultMyModel.Anchors MyModel._generate_anchors()def save_tensor(tensor, path):保存 tensor 对象到文件torch.save(tensor, gzip.GzipFile(path, wb))def load_tensor(path):从文件读取 tensor 对象return torch.load(gzip.GzipFile(path, rb))def calc_resize_parameters(sw, sh):计算缩放图片的参数sw_new, sh_new sw, shdw, dh IMAGE_SIZEpad_w, pad_h 0, 0if sw / sh dw / dh:sw_new int(dw / dh * sh)pad_w (sw_new - sw) // 2 # 填充左右else:sh_new int(dh / dw * sw)pad_h (sh_new - sh) // 2 # 填充上下return sw_new, sh_new, pad_w, pad_hdef resize_image(img):缩放图片比例不一致时填充sw, sh img.sizesw_new, sh_new, pad_w, pad_h calc_resize_parameters(sw, sh)img_new Image.new(RGB, (sw_new, sh_new))img_new.paste(img, (pad_w, pad_h))img_new img_new.resize(IMAGE_SIZE)return img_newdef image_to_tensor(img):转换图片对象到 tensor 对象arr numpy.asarray(img)t torch.from_numpy(arr)t t.transpose(0, 2) # 转换维度 H,W,C 到 C,W,Ht t / 255.0 # 正规化数值使得范围在 0 ~ 1return tdef map_box_to_resized_image(box, sw, sh):把原始区域转换到缩放后的图片对应的区域x, y, w, h boxsw_new, sh_new, pad_w, pad_h calc_resize_parameters(sw, sh)scale IMAGE_SIZE[0] / sw_newx int((x pad_w) * scale)y int((y pad_h) * scale)w int(w * scale)h int(h * scale)if x w IMAGE_SIZE[0] or y h IMAGE_SIZE[1] or w 0 or h 0:return 0, 0, 0, 0return x, y, w, hdef map_box_to_original_image(box, sw, sh):把缩放后图片对应的区域转换到缩放前的原始区域x, y, w, h boxsw_new, sh_new, pad_w, pad_h calc_resize_parameters(sw, sh)scale IMAGE_SIZE[0] / sw_newx int(x / scale - pad_w)y int(y / scale - pad_h)w int(w / scale)h int(h / scale)if x w sw or y h sh or x 0 or y 0 or w 0 or h 0:return 0, 0, 0, 0return x, y, w, hdef calc_iou(rect1, rect2):计算两个区域重叠部分 / 合并部分的比率 (intersection over union)x1, y1, w1, h1 rect1x2, y2, w2, h2 rect2xi max(x1, x2)yi max(y1, y2)wi min(x1w1, x2w2) - xihi min(y1h1, y2h2) - yiif wi 0 and hi 0: # 有重叠部分area_overlap wi*hiarea_all w1*h1 w2*h2 - area_overlapiou area_overlap / area_allelse: # 没有重叠部分iou 0return ioudef calc_box_offset(candidate_box, true_box):计算候选区域与实际区域的偏移值要求实际区域的中心点必须在候选区域中# 计算实际区域的中心点在候选区域中的位置范围会在 0 ~ 1 之间x1, y1, w1, h1 candidate_boxx2, y2, w2, h2 true_boxx_offset ((x2 w2 // 2) - x1) / w1y_offset ((y2 h2 // 2) - y1) / h1# 计算实际区域长宽相对于候选区域长宽的比例使用 log 减少过大的值w_offset math.log(w2 / w1)h_offset math.log(h2 / h1)return (x_offset, y_offset, w_offset, h_offset)def adjust_box_by_offset(candidate_box, offset):根据偏移值调整候选区域x1, y1, w1, h1 candidate_boxx_offset, y_offset, w_offset, h_offset offsetw2 math.exp(w_offset) * w1h2 math.exp(h_offset) * h1x2 x1 w1 * x_offset - w2 // 2y2 y1 h1 * y_offset - h2 // 2x2 min(IMAGE_SIZE[0]-1, x2)y2 min(IMAGE_SIZE[1]-1, y2)w2 min(IMAGE_SIZE[0]-x2, w2)h2 min(IMAGE_SIZE[1]-y2, h2)return (x2, y2, w2, h2)def prepare_save_batch(batch, image_tensors, result_tensors, result_isobject_masks, result_nonobject_masks):准备训练 - 保存单个批次的数据# 按索引值列表生成输入和输出 tensor 对象的函数def split_dataset(indices):indices_list indices.tolist()image_tensors_splited torch.stack([image_tensors[x] for x in indices_list])result_tensors_splited torch.stack([result_tensors[x] for x in indices_list])result_isobject_masks_splited [result_isobject_masks[x] for x in indices_list]result_nonobject_masks_splited [result_nonobject_masks[x] for x in indices_list]return image_tensors_splited, (result_tensors_splited, result_isobject_masks_splited, result_nonobject_masks_splited)# 切分训练集 (80%)验证集 (10%) 和测试集 (10%)random_indices torch.randperm(len(image_tensors))training_indices random_indices[:int(len(random_indices)*0.8)]validating_indices random_indices[int(len(random_indices)*0.8):int(len(random_indices)*0.9):]testing_indices random_indices[int(len(random_indices)*0.9):]training_set split_dataset(training_indices)validating_set split_dataset(validating_indices)testing_set split_dataset(testing_indices)# 保存到硬盘save_tensor(training_set, fdata/training_set.{batch}.pt)save_tensor(validating_set, fdata/validating_set.{batch}.pt)save_tensor(testing_set, fdata/testing_set.{batch}.pt)print(fbatch {batch} saved)def prepare():准备训练# 数据集转换到 tensor 以后会保存在 data 文件夹下if not os.path.isdir(data):os.makedirs(data)# 加载图片和图片对应的区域与分类列表# { (路径, 是否左右翻转): [ 区域与分类, 区域与分类, .. ] }# 同一张图片左右翻转可以生成一个新的数据让数据量翻倍box_map defaultdict(lambda: [])for filename in os.listdir(DATASET_1_IMAGE_DIR):# 从第一个数据集加载xml_path os.path.join(DATASET_1_ANNOTATION_DIR, filename.split(.)[0] .xml)if not os.path.isfile(xml_path):continuetree ET.ElementTree(filexml_path)objects tree.findall(object)path os.path.join(DATASET_1_IMAGE_DIR, filename)for obj in objects:class_name obj.find(name).textx1 int(obj.find(bndbox/xmin).text)x2 int(obj.find(bndbox/xmax).text)y1 int(obj.find(bndbox/ymin).text)y2 int(obj.find(bndbox/ymax).text)if class_name mask_weared_incorrect:# 佩戴口罩不正确的样本数量太少 (只有 123)模型无法学习这里全合并到戴口罩的样本class_name with_maskbox_map[(path, False)].append((x1, y1, x2-x1, y2-y1, CLASSES_MAPPING[class_name]))box_map[(path, True)].append((x1, y1, x2-x1, y2-y1, CLASSES_MAPPING[class_name]))df pandas.read_csv(DATASET_2_BOX_CSV_PATH)for row in df.values:# 从第二个数据集加载这个数据集只包含没有带口罩的图片filename, width, height, x1, y1, x2, y2 row[:7]path os.path.join(DATASET_2_IMAGE_DIR, filename)box_map[(path, False)].append((x1, y1, x2-x1, y2-y1, CLASSES_MAPPING[without_mask]))box_map[(path, True)].append((x1, y1, x2-x1, y2-y1, CLASSES_MAPPING[without_mask]))# 打乱数据集 (因为第二个数据集只有不戴口罩的图片)box_list list(box_map.items())random.shuffle(box_list)print(ffound {len(box_list)} images)# 保存图片和图片对应的分类与区域列表batch_size 20batch 0image_tensors [] # 图片列表result_tensors [] # 图片对应的输出结果列表包含 [ 是否对象中心, 区域偏移, 各个分类的可能性 ]result_isobject_masks [] # 各个图片的包含对象的区域在 Anchors 中的索引result_nonobject_masks [] # 各个图片不包含对象的区域在 Anchors 中的索引 (重叠率低于阈值的区域)for (image_path, flip), original_boxes_labels in box_list:with Image.open(image_path) as img_original: # 加载原始图片sw, sh img_original.size # 原始图片大小if flip:img resize_image(img_original.transpose(Image.FLIP_LEFT_RIGHT)) # 翻转然后缩放图片else:img resize_image(img_original) # 缩放图片image_tensors.append(image_to_tensor(img)) # 添加图片到列表# 生成输出结果的 tensorresult_tensor torch.zeros((len(MyModel.Anchors), MyModel.AnchorOutputs), dtypetorch.float)result_tensor[:,5] 1 # 默认分类为 otherresult_tensors.append(result_tensor)# 包含对象的区域在 Anchors 中的索引result_isobject_mask []result_isobject_masks.append(result_isobject_mask)# 不包含对象的区域在 Anchors 中的索引result_nonobject_mask []result_nonobject_masks.append(result_nonobject_mask)# 根据真实区域定位所属的锚点然后设置输出结果negative_mapping [1] * len(MyModel.Anchors)for box_label in original_boxes_labels:x, y, w, h, label box_labelif flip: # 翻转坐标x sw - x - wx, y, w, h map_box_to_resized_image((x, y, w, h), sw, sh) # 缩放实际区域if w 20 or h 20:continue # 缩放后区域过小# 检查计算是否有问题# child_img img.copy().crop((x, y, xw, yh))# child_img.save(f{os.path.basename(image_path)}_{x}_{y}_{w}_{h}_{label}.png)# 定位所属的锚点# 要求:# - 中心点落在锚点对应的区域中# - 重叠率超过一定值x_center x w // 2y_center y h // 2matched_anchors []for index, anchor in enumerate(MyModel.Anchors):ax, ay, aw, ah anchoris_center (x_center ax and x_center ax aw andy_center ay and y_center ay ah)iou calc_iou(anchor, (x, y, w, h))if is_center and iou IOU_POSITIVE_THRESHOLD:matched_anchors.append((index, anchor)) # 区域包含对象中心并且重叠率超过一定值negative_mapping[index] 0elif iou IOU_NEGATIVE_THRESHOLD:negative_mapping[index] 0 # 区域与某个对象重叠率超过一定值不应该当作负样本for matched_index, matched_box in matched_anchors:# 计算区域偏移offset calc_box_offset(matched_box, (x, y, w, h))# 修改输出结果的 tensorresult_tensor[matched_index] torch.tensor((1, # 是否对象中心*offset, # 区域偏移*[int(c label) for c in range(len(CLASSES))] # 对应分类), dtypetorch.float)# 添加索引值# 注意如果两个对象同时定位到相同的锚点那么只有一个对象可以被识别这里后面的对象会覆盖前面的对象if matched_index not in result_isobject_mask:result_isobject_mask.append(matched_index)# 没有找到可识别的对象时跳过图片if not result_isobject_mask:image_tensors.pop()result_tensors.pop()result_isobject_masks.pop()result_nonobject_masks.pop()continue# 添加不包含对象的区域在 Anchors 中的索引for index, value in enumerate(negative_mapping):if value:result_nonobject_mask.append(index)# 排序索引列表result_isobject_mask.sort()# 保存批次if len(image_tensors) batch_size:prepare_save_batch(batch, image_tensors, result_tensors,result_isobject_masks, result_nonobject_masks)image_tensors.clear()result_tensors.clear()result_isobject_masks.clear()result_nonobject_masks.clear()batch 1# 保存剩余的批次if len(image_tensors) 10:prepare_save_batch(batch, image_tensors, result_tensors,result_isobject_masks, result_nonobject_masks)def train():开始训练# 创建模型实例model MyModel().to(device)# 创建多任务损失计算器loss_function MyModel.loss_function# 创建参数调整器optimizer torch.optim.Adam(model.parameters())# 记录训练集和验证集的正确率变化training_obj_accuracy_history []training_cls_accuracy_history []validating_obj_accuracy_history []validating_cls_accuracy_history []# 记录最高的验证集正确率validating_obj_accuracy_highest -1validating_cls_accuracy_highest -1validating_accuracy_highest -1validating_accuracy_highest_epoch 0# 读取批次的工具函数def read_batches(base_path):for batch in itertools.count():path f{base_path}.{batch}.ptif not os.path.isfile(path):breakx, (y, mask1, mask2) load_tensor(path)yield x.to(device), (y.to(device), mask1, mask2)# 计算正确率的工具函数calc_accuracy MyModel.calc_accuracy# 开始训练过程for epoch in range(1, 10000):print(fepoch: {epoch})# 根据训练集训练并修改参数# 切换模型到训练模式将会启用自动微分批次正规化 (BatchNorm) 与 Dropoutmodel.train()training_obj_accuracy_list []training_cls_accuracy_list []for batch_index, batch in enumerate(read_batches(data/training_set)):# 划分输入和输出batch_x, batch_y batch# 计算预测值predicted model(batch_x)# 计算损失loss loss_function(predicted, batch_y)# 从损失自动微分求导函数值loss.backward()# 使用参数调整器调整参数optimizer.step()# 清空导函数值optimizer.zero_grad()# 记录这一个批次的正确率torch.no_grad 代表临时禁用自动微分功能with torch.no_grad():training_batch_obj_accuracy, training_batch_cls_accuracy calc_accuracy(batch_y, predicted)# 输出批次正确率training_obj_accuracy_list.append(training_batch_obj_accuracy)training_cls_accuracy_list.append(training_batch_cls_accuracy)print(fepoch: {epoch}, batch: {batch_index}: fbatch obj accuracy: {training_batch_obj_accuracy}, cls accuracy: {training_batch_cls_accuracy})training_obj_accuracy sum(training_obj_accuracy_list) / len(training_obj_accuracy_list)training_cls_accuracy sum(training_cls_accuracy_list) / len(training_cls_accuracy_list)training_obj_accuracy_history.append(training_obj_accuracy)training_cls_accuracy_history.append(training_cls_accuracy)print(ftraining obj accuracy: {training_obj_accuracy}, cls accuracy: {training_cls_accuracy})# 检查验证集# 切换模型到验证模式将会禁用自动微分批次正规化 (BatchNorm) 与 Dropoutmodel.eval()validating_obj_accuracy_list []validating_cls_accuracy_list []for batch in read_batches(data/validating_set):batch_x, batch_y batchpredicted model(batch_x)validating_batch_obj_accuracy, validating_batch_cls_accuracy calc_accuracy(batch_y, predicted)validating_obj_accuracy_list.append(validating_batch_obj_accuracy)validating_cls_accuracy_list.append(validating_batch_cls_accuracy)# 释放 predicted 占用的显存避免显存不足的错误predicted Nonevalidating_obj_accuracy sum(validating_obj_accuracy_list) / len(validating_obj_accuracy_list)validating_cls_accuracy sum(validating_cls_accuracy_list) / len(validating_cls_accuracy_list)validating_obj_accuracy_history.append(validating_obj_accuracy)validating_cls_accuracy_history.append(validating_cls_accuracy)print(fvalidating obj accuracy: {validating_obj_accuracy}, cls accuracy: {validating_cls_accuracy})# 记录最高的验证集正确率与当时的模型状态判断是否在 20 次训练后仍然没有刷新记录validating_accuracy validating_obj_accuracy * validating_cls_accuracyif validating_accuracy validating_accuracy_highest:validating_obj_accuracy_highest validating_obj_accuracyvalidating_cls_accuracy_highest validating_cls_accuracyvalidating_accuracy_highest validating_accuracyvalidating_accuracy_highest_epoch epochsave_tensor(model.state_dict(), model.pt)print(highest validating accuracy updated)elif epoch - validating_accuracy_highest_epoch 20:# 在 20 次训练后仍然没有刷新记录结束训练print(stop training because highest validating accuracy not updated in 20 epoches)break# 使用达到最高正确率时的模型状态print(fhighest obj validating accuracy: {validating_obj_accuracy_highest},ffrom epoch {validating_accuracy_highest_epoch})print(fhighest cls validating accuracy: {validating_cls_accuracy_highest},ffrom epoch {validating_accuracy_highest_epoch})model.load_state_dict(load_tensor(model.pt))# 检查测试集testing_obj_accuracy_list []testing_cls_accuracy_list []for batch in read_batches(data/testing_set):batch_x, batch_y batchpredicted model(batch_x)testing_batch_obj_accuracy, testing_batch_cls_accuracy calc_accuracy(batch_y, predicted)testing_obj_accuracy_list.append(testing_batch_obj_accuracy)testing_cls_accuracy_list.append(testing_batch_cls_accuracy)testing_obj_accuracy sum(testing_obj_accuracy_list) / len(testing_obj_accuracy_list)testing_cls_accuracy sum(testing_cls_accuracy_list) / len(testing_cls_accuracy_list)print(ftesting obj accuracy: {testing_obj_accuracy}, cls accuracy: {testing_cls_accuracy})# 显示训练集和验证集的正确率变化pyplot.plot(training_obj_accuracy_history, labeltraining_obj_accuracy)pyplot.plot(training_cls_accuracy_history, labeltraining_cls_accuracy)pyplot.plot(validating_obj_accuracy_history, labelvalidating_obj_accuracy)pyplot.plot(validating_cls_accuracy_history, labelvalidating_cls_accuracy)pyplot.ylim(0, 1)pyplot.legend()pyplot.show()def eval_model():使用训练好的模型识别图片# 创建模型实例加载训练好的状态然后切换到验证模式model MyModel().to(device)model.load_state_dict(load_tensor(model.pt))model.eval()# 询问图片路径并显示所有可能是人脸的区域while True:try:image_path input(Image path: )if not image_path:continue# 构建输入with Image.open(image_path) as img_original: # 加载原始图片sw, sh img_original.size # 原始图片大小img resize_image(img_original) # 缩放图片img_output img_original.copy() # 复制图片用于后面添加标记tensor_in image_to_tensor(img)# 预测输出predicted model(tensor_in.unsqueeze(0).to(device))[0]final_result MyModel.convert_predicted_result(predicted)# 标记在图片上draw ImageDraw.Draw(img_output)for label, box, obj_score, cls_score in final_result:x, y, w, h map_box_to_original_image(box, sw, sh)score obj_score * cls_scorecolor #00FF00 if CLASSES[label] with_mask else #FF0000draw.rectangle((x, y, xw, yh), outlinecolor)draw.text((x, y-10), CLASSES[label], fillcolor)draw.text((x, yh), f{score:.2f}, fillcolor)print((x, y, w, h), CLASSES[label], obj_score, cls_score)img_output.save(img_output.png)print(saved to img_output.png)print()except Exception as e:print(error:, e)def eval_video():使用训练好的模型识别视频# 创建模型实例加载训练好的状态然后切换到验证模式model MyModel().to(device)model.load_state_dict(load_tensor(model.pt))model.eval()# 询问视频路径给可能是人脸的区域添加标记并保存新视频import cv2font ImageFont.truetype(FreeMonoBold.ttf, 20)while True:try:video_path input(Video path: )if not video_path:continue# 读取输入视频video cv2.VideoCapture(video_path)# 获取每秒的帧数fps int(video.get(cv2.CAP_PROP_FPS))# 获取视频长宽size (int(video.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)))# 创建输出视频video_output_path os.path.join(os.path.dirname(video_path),os.path.splitext(os.path.basename(video_path))[0] .output.avi)result cv2.VideoWriter(video_output_path, cv2.VideoWriter_fourcc(*XVID), fps, size)# 用于减少误判的历史结果history_results deque(maxlen fps // 2)# 逐帧处理count 0while(True):ret, frame video.read()if not ret:break# opencv 使用的是 BGR, Pillow 使用的是 RGB, 需要转换通道顺序frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)# 构建输入img_original Image.fromarray(frame_rgb) # 加载原始图片sw, sh img_original.size # 原始图片大小img resize_image(img_original) # 缩放图片img_output img_original.copy() # 复制图片用于后面添加标记tensor_in image_to_tensor(img)# 预测输出predicted model(tensor_in.unsqueeze(0).to(device))[0]cls_result MyModel.convert_predicted_result(predicted)# 根据历史结果减少误判final_result MyModel.fix_predicted_result_from_history(cls_result, history_results)# 标记在图片上draw ImageDraw.Draw(img_output)for label, box, obj_score, cls_score in final_result:x, y, w, h map_box_to_original_image(box, sw, sh)score obj_score * cls_scorecolor #00FF00 if CLASSES[label] with_mask else #FF0000draw.rectangle((x, y, xw, yh), outlinecolor, width3)draw.text((x, y-20), CLASSES[label], fillcolor, fontfont)draw.text((x, yh), f{score:.2f}, fillcolor, fontfont)# 写入帧到输出视频frame_rgb_annotated numpy.asarray(img_output)frame_bgr_annotated cv2.cvtColor(frame_rgb_annotated, cv2.COLOR_RGB2BGR)result.write(frame_bgr_annotated)count 1if count % fps 0:print(fhandled {count//fps}s)video.release()result.release()cv2.destroyAllWindows()print(fsaved to {video_output_path})print()except Exception as e:raiseprint(error:, e)def main():主函数if len(sys.argv) 2:print(fPlease run: {sys.argv[0]} prepare|train|eval)exit()# 给随机数生成器分配一个初始值使得每次运行都可以生成相同的随机数# 这是为了让过程可重现你也可以选择不这样做random.seed(0)torch.random.manual_seed(0)# 根据命令行参数选择操作operation sys.argv[1]if operation prepare:prepare()elif operation train:train()elif operation eval:eval_model()elif operation eval-video:eval_video()else:raise ValueError(fUnsupported operation: {operation})if __name__ __main__:main()​ 2.5 检测效果 检测效果如下可以看到效果还是很好的 实时检测效果 3 口罩佩戴检测算法评价指标 目前 口罩佩戴识别的研究已经成为热点 算法也是越来越多 所以要判断算法的好坏还需要一些指标作为参考 [36]。 大部分的评价指标都来自对混淆矩阵confusion matrix 的计算。 其中 PPositive 表示预测值为正例。 NNegative表示预测值为反例。 TTrue 表示真实值与预测值相同。 预测值与真实值相反记为 FFalse 。 TP 表示真实值是正样本或者说预测值为正样本 真实值和预测值相同。 TN 则为真实值为负样本或者说预测值为负样本 并且真实值和预测值相同。 FP 则为真实值为负样本或者说预测值为正样本 真实值和预测值不一样。 FN 则为真实值为正样本或者说预测值为负样本 并且真实值和预测值不一样。 3.1 准确率Accuracy 表示的是模型在检测时判断为正样本与所有样本的比例准确率通常用来评价整个模型的准确程度 且不会包含太多信息 因此不可能对模型的性能进行综合评价 3.2 精确率(Precision)和召回率(Recall) 精确率表示的是预测为正样本中真正正样本所占的比例 它反映的是预测结果准确与否。 精确率的计算公式如下所示 召回率表明样本中正例被预测正确的多少 召回率主要看的是预测的结果是否全面。 召回率的计算公式如2-3 所示 3.3 平均精度(Average precision AP) 表示每个类检测好坏的结果。 召回率被当作横坐标 精确率被当作纵坐标 表示 P-R 曲线下方的面积 AP 值越高表明模型的平均准确率越高。 AP 的计算公式如下所示 4 最后 更多资料, 项目分享 https://gitee.com/dancheng-senior/postgraduate

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

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

相关文章

做模板网站的利与弊做网站的哪个好

网上各种标为2013年,实际上都是2012年或者更早的,下面的才是真正的2013年5月5日考试的卷子。 答题说明: 1.答题时间90分钟,请注意把握时间; 2.试题分为四个部分:单项选择题(10题,20分…

网站排名优化培训电话网页创意设计

最近,德迅云安全遇到不少网站用户遇到攻击问题,来咨询安全解决方案。目前在所有的网络攻击方式中,DDoS是最常见,也是最高频的攻击方式之一。不少用户网站上线后,经常会遭受到攻击的困扰。有些攻击持续时间比较短影响较…

网站建设的基本特点临沂市建设局网站

问答网是一款为IT工程师提供的问答平台,旨在帮助用户在线获取专业知识和相关问题的答案。在问答网,用户可以轻松找到其他人的问答问题,并在这里寻求解答。如果您有任何想要解决的问题,都可以在此发布问题并得到其他同行的解答。 …

做视频网站的备案要求郑州专业网站制作费用报价

变量,指针,引用 //拷贝与拷贝构造函数 //拷贝(copy):拷贝数据,拷贝内存 //始终是在拷贝值,但是指针存储的是内存的地址,变量存储的是数据的值 //特别注意,在引用里面的拷…

电商网站后台建设网站广告销售怎们做

1.百度T7跳槽字节3-1,总包145万,压力太大想降级 硕士毕业工作10年,一百度T7大头兵发文称,自己最近拿到字节3-1的offer,年包从现有的110万涨30%到145万。但是担心去字节后因为定的职级高需要带人,压力会很大…

双线网站选服务器网络推广公司怎么找客户

TypeScript 技术文档 目录 TypeScript 技术文档1. 简介2. 安装与配置3. 基本类型3.1 布尔值3.2 数字3.3 字符串3.4 数组3.5 元组3.6 枚举3.7 Any3.8 Void3.9 Null 和 Undefined3.10 Never3.11 Object 4. 接口4.1 简单示例4.2 可选属性4.3 只读属性4.4 函数类型4.5 可索引类型 5…

阿玛尼高仿手表网站百度精准推广

linux(ubuntu/centos)、windows安装php-zip扩展 PHP安装zip拓展,以及libzip安装问题

做图文的网站开发公司主体灭失曾经的备案是否有效

JavaScript中,for循环、for...in循环和for...of循环是用于迭代数组或对象属性的不同方式。 for循环: for循环是最常见的迭代方法,它允许你指定迭代的起始点、结束条件和每次迭代后的操作。它可以用于迭代数组和字符串。 例如,遍…

做网站算法百度收录提交网址

线程池的分类 在 Java 中,常用的线程池有以下几种: ThreadPoolExecutor:ThreadPoolExecutor 是 Java 提供的最基本的线程池实现。它提供了丰富的参数配置,可以自定义核心线程数、最大线程数、线程空闲时间、工作队列等。 Execut…

西安网站群建设广州市安全平台

第一步:创建项目的时候选择ASP.NET Croe Web API 点击下一步,然后配置: 下一步:

具有价值的专业网站建设平台昆明企业制作网站

最近项目上试运行发现,很多时候网站出了问题或者某个功能不正常,常常需要运维人员去服务器里面查看一下日志,看看日志里面会产生什么异常,这样导致每次都要去远程服务器很不方便,有时服务器是客户保管的不能让我们随意…

网站外链要怎么做微信外链网站

目录 说明工具准备工具配置jmeter 界面汉化配置汉化步骤汉化结果图 案例1:测试接口接口准备线程组添加线程组配置线程组值线程数(Number of Threads)Ramp-Up 时间(Ramp-Up Period)循环次数(Loop Count&…

地方网站欣赏wordpress根目录在

背景 传统的图像生成模型有GAN,VAE等,但是存在模式坍缩,即生成图片缺乏多样性,这是因为模型本身结构导致的。而扩散模型拥有训练稳定,保持图像多样性等特点,逐渐成为现在AIGC领域的主流。 扩散模型 正如…

介绍自己的做的网站吗新冠疫苗接种查询

前不久,有个关于华为云 CloudIDE 的问题在知乎、朋友圈、微博等圈子引起了广泛的讨论,甚至上了知乎热榜。那么,背后的真实情况到底是如何的?且听韩老师娓娓道来。华为云 CloudIDE 酷似 VS Code?首先要明确一点&#xf…

手机建设网站目的wordpress菜单外观样式

观测云更新 日志 数据转发:新增外部存储转发规则数据查询;支持启用/禁用转发规则;绑定索引:日志易新增标签绑定,从而实现更细颗粒度的数据范围查询授权能力。 基础设施 > 自定义 【默认属性】这一概念更改为【必…

长春模板建站代理网站开发先找UI吗

文章目录 1 docker学习1.1 基本命令使用1.1.1 docker ps查看当前正在运行的镜像1.1.2 docker stop停止容器1.1.3 docker compose容器编排1.1.4 docker网络[1] 进入到容器里面敲命令[2] docker network ls[3] brige网络模式下容器访问宿主机的方式 2 Dify的安装和基础使用2.1 下…

深圳网站建设推广教学网站前台模板

问题描述 使用python的requests库去发送https请求,有时候不设置verifyFalse不报错,有时候又报错。 问题原因 使用Python的requests库发送HTTPS请求时,设置verifyFalse参数可以跳过SSL证书验证。默认情况下,requests库会验证SSL…

响应式企业网站模板建设带数据搜索的网站

当别⼈犯错误,⽽你不犯销误时,你就赢了。 如果我们把⽬光放得⽐较长远,就会发现重视经验和传统的做法会更好。 理性主义的好处是能够找出世界的共性,因此它通常显示出很⾼的效率,特别是短时间内的效率。 最然依赖经…

四川省建设厅网站证如何做网站卖画

文章目录 用例编写 用例编写 用例名称:【版本号】页面-页面,功能校验所属模块:2023年/一季度/版本号前置条件: 用户登录管理后台依次点击菜单:仓库管理—员工管理 步骤描述: 点击 [] 按钮 1.1.1 1.2.2 点…