项目参考AAAI Association for the Advancement of Artificial Intelligence
研究背景与意义
随着人工智能技术的快速发展,计算机视觉在各个领域中的应用越来越广泛。其中,物体检测是计算机视觉领域中的一个重要研究方向。物体检测技术可以帮助我们自动识别和定位图像中的目标物体,为各种应用场景提供支持,如智能监控、自动驾驶、工业质检等。
木材缺陷检测是工业质检中的一个重要环节。传统的木材缺陷检测通常依赖于人工目视检查,这种方法存在着人力成本高、效率低、主观性强等问题。因此,开发一种高效准确的自动化木材缺陷检测系统具有重要的实际意义。
目前,基于深度学习的物体检测方法已经取得了显著的进展。YOLO(You Only Look Once)是一种非常流行的物体检测算法,其以其快速的检测速度和较高的准确率而受到广泛关注。然而,YOLO在处理小目标物体时存在一定的困难,这主要是由于小目标物体的特点导致的。小目标物体通常具有较低的分辨率和较少的上下文信息,这使得它们更容易被忽略或错误地分类。
为了解决YOLO在小目标物体检测中的问题,一种新的方法被提出,即融合NWD_loss的YOLO的木材缺陷检测系统。NWD_loss是一种基于注意力机制的损失函数,它可以帮助网络更好地关注小目标物体的特征。通过引入NWD_loss,我们可以提高YOLO在小目标物体检测中的准确率和鲁棒性。
该研究的主要目标是开发一种高效准确的木材缺陷检测系统,以提高木材质检的效率和准确性。具体来说,我们的研究将重点关注以下几个方面:
首先,我们将探索如何融合NWD_loss到YOLO中,以提高其在小目标物体检测中的性能。通过引入注意力机制,我们可以使网络更加关注小目标物体的特征,从而提高检测的准确率。
其次,我们将设计和实现一个高效的木材缺陷数据集,以评估我们提出的方法的性能。该数据集将包含大量的真实木材图像,并且涵盖各种不同类型的缺陷,如裂纹、疤痕、虫蛀等。通过使用这个数据集,我们可以充分评估我们的方法在真实场景中的表现。
最后,我们将进行大量的实验和对比分析,以验证我们提出的方法的有效性和优越性。我们将与其他常用的木材缺陷检测方法进行比较,并评估我们的方法在准确率、召回率、速度等方面的性能。
总之,本研究旨在开发一种高效准确的木材缺陷检测系统,以提高木材质检的效率和准确性。通过融合NWD_loss到YOLO中,我们可以提高其在小目标物体检测中的性能。这将对工业质检领域的自动化和智能化发展具有重要的推动作用。
【小目标SOTA】融合NWD_loss的YOLO的木材缺陷检测系统_哔哩哔哩_bilibili
首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集Wooddefects。
labelImg是一个图形化的图像注释工具,支持VOC和YOLO格式。以下是使用labelImg将图片标注为VOC格式的步骤:
(1)下载并安装labelImg。
(2)打开labelImg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的XML文件。
(6)重复此过程,直到所有的图片都标注完毕。
由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。
下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
import os
classes = [] # 初始化为空列表
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
def convert(size, box):
dw = 1. / size[0]
dh = 1. / size[1]
x = (box[0] + box[1]) / 2.0
y = (box[2] + box[3]) / 2.0
w = box[1] - box[0]
h = box[3] - box[2]
x = x * dw
w = w * dw
y = y * dh
h = h * dh
return (x, y, w, h)
def convert_annotation(image_id):
in_file = open('./label_xml\%s.xml' % (image_id), encoding='UTF-8')
out_file = open('./label_txt\%s.txt' % (image_id), 'w') # 生成txt格式文件
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
for obj in root.iter('object'):
cls = obj.find('name').text
if cls not in classes:
classes.append(cls) # 如果类别不存在,添加到classes列表中
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
bb = convert((w, h), b)
out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
xml_path = os.path.join(CURRENT_DIR, './label_xml/')
# xml list
img_xmls = os.listdir(xml_path)
for img_xml in img_xmls:
label_name = img_xml.split('.')[0]
print(label_name)
convert_annotation(label_name)
print("Classes:") # 打印最终的classes列表
print(classes) # 打印最终的classes列表
我们需要将数据集整理为以下结构:
-----data
|-----train
| |-----images
| |-----labels
|
|-----valid
| |-----images
| |-----labels
|
|-----test
|-----images
|-----labels
确保以下几点:
所有的训练图片都位于data/train/images目录下,相应的标注文件位于data/train/labels目录下。
所有的验证图片都位于data/valid/images目录下,相应的标注文件位于data/valid/labels目录下。
所有的测试图片都位于data/test/images目录下,相应的标注文件位于data/test/labels目录下。
这样的结构使得数据的管理和模型的训练、验证和测试变得非常方便。
Epoch gpu_mem box obj cls labels img_size
1/200 20.8G 0.01576 0.01955 0.007536 22 1280: 100%|██████████| 849/849 [14:42<00:00, 1.04s/it]
Class Images Labels P R [email protected] [email protected]:.95: 100%|██████████| 213/213 [01:14<00:00, 2.87it/s]
all 3395 17314 0.994 0.957 0.0957 0.0843
Epoch gpu_mem box obj cls labels img_size
2/200 20.8G 0.01578 0.01923 0.007006 22 1280: 100%|██████████| 849/849 [14:44<00:00, 1.04s/it]
Class Images Labels P R [email protected] [email protected]:.95: 100%|██████████| 213/213 [01:12<00:00, 2.95it/s]
all 3395 17314 0.996 0.956 0.0957 0.0845
Epoch gpu_mem box obj cls labels img_size
3/200 20.8G 0.01561 0.0191 0.006895 27 1280: 100%|██████████| 849/849 [10:56<00:00, 1.29it/s]
Class Images Labels P R [email protected] [email protected]:.95: 100%|███████ | 187/213 [00:52<00:00, 4.04it/s]
all 3395 17314 0.996 0.957 0.0957 0.0845
class ComputeLoss:
sort_obj_iou = False
# Compute losses
def __init__(self, model, autobalance=False):
device = next(model.parameters()).device # get model device
h = model.hyp # hyperparameters
# Define criteria
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device))
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device))
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets
# Focal loss
g = h['fl_gamma'] # focal loss gamma
if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
m = de_parallel(model).model[-1] # Detect() module
self.balance = {3: [4.0, 1.0, 0.4]}.get(m.nl, [4.0, 1.0, 0.25, 0.06, 0.02]) # P3-P7
self.ssi = list(m.stride).index(16) if autobalance else 0 # stride 16 index
self.BCEcls, self.BCEobj, self.gr, self.hyp, self.autobalance = BCEcls, BCEobj, 1.0, h, autobalance
self.na = m.na # number of anchors
self.nc = m.nc # number of classes
self.nl = m.nl # number of layers
self.anchors = m.anchors
self.device = device
def __call__(self, p, targets): # predictions, targets
lcls = torch.zeros(1, device=self.device) # class loss
lbox = torch.zeros(1, device=self.device) # box loss
lobj = torch.zeros(1, device=self.device) # object loss
tcls, tbox, indices, anchors = self.build_targets(p, targets) # targets
# Losses
for i, pi in enumerate(p): # layer index, layer predictions
b, a, gj, gi = indices[i] # image, anchor, gridy, gridx
tobj = torch.zeros(pi.shape[:4], dtype=pi.dtype, device=self.device) # target obj
n = b.shape[0] # number of targets
if n:
pxy, pwh, _, pcls = pi[b, a, gj, gi].split((2, 2, 1, self
......
该文件名为loss.py,是一个用于计算损失函数的程序文件。
该文件中定义了多个损失函数类,包括smooth_BCE、BCEBlurWithLogitsLoss、FocalLoss、QFocalLoss和wasserstein_loss。
其中smooth_BCE函数用于计算二分类的平滑BCE损失函数。
BCEBlurWithLogitsLoss类是对BCEwithLogitLoss()的一个封装,用于减少缺失标签的影响。
FocalLoss类是对现有的损失函数进行封装,用于计算focal loss。
QFocalLoss类是对现有的损失函数进行封装,用于计算quality focal loss。
wasserstein_loss函数是根据论文《Enhancing Geometric Factors into Model Learning and Inference for Object Detection and Instance Segmentation》中的方法实现的损失函数。
最后,ComputeLoss类用于计算损失函数。在初始化时,会根据模型的超参数和设备信息定义一些变量和损失函数。在调用时,会根据预测结果和目标值计算损失,并返回总的损失和各个部分的损失。同时,还会根据计算得到的损失调整权重。
class YOLOv5Trainer:
def __init__(self, hyp, opt, device, callbacks):
self.hyp = hyp
self.opt = opt
self.device = device
self.callbacks = callbacks
def train(self):
# code for training
pass
def _create_directories(self):
# code for creating directories
pass
def _load_hyperparameters(self):
# code for loading hyperparameters
pass
def _save_run_settings(self):
# code for saving run settings
pass
def _create_loggers(self):
# code for creating loggers
pass
def _process_custom_dataset(self):
# code for processing custom dataset
pass
def _load_model(self):
# code for loading model
pass
def _freeze_layers(self):
# code for freezing layers
pass
def _verify_image_size(self):
# code for verifying image size
pass
def _estimate_batch_size(self):
# code for estimating batch size
pass
def _create_optimizer(self):
# code for creating optimizer
pass
def _create_scheduler(self):
# code for creating scheduler
pass
def _create_ema(self):
# code for creating EMA
pass
def _resume_training(self):
# code for resuming training
pass
def _dp_mode(self):
# code for DP mode
pass
def _sync_batchnorm(self):
# code for SyncBatchNorm
pass
train.py是一个用于训练YOLOv5模型的程序文件。它可以在自定义数据集上训练YOLOv5模型,并支持单GPU和多GPU分布式训练。
程序文件中包含了一些命令行参数,可以通过命令行来指定训练所需的参数,例如数据集路径、模型权重、训练时的图片尺寸等。
训练过程中会自动下载YOLOv5模型和数据集,并根据指定的参数进行训练。训练过程中会保存模型权重和训练日志,可以通过指定的保存目录来查看。
程序文件还包含了一些辅助函数和工具类,用于处理数据集、模型加载、优化器设置、学习率调整等操作。
总体来说,train.py是一个功能完善的训练脚本,可以方便地在自定义数据集上训练YOLOv5模型。
def load_model(
weights='./best.pt', # model.pt path(s)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference
):
# Load model
device = select_device(device)
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data)
stride, names, pt, jit, onnx, engine = model.stride, model.names, model.pt, model.jit, model.onnx, model.engine
# Half
half &= (pt or jit or onnx or engine) and device.type != 'cpu' # FP16 supported on limited backends with CUDA
if pt or jit:
model.model.half() if half else model.model.float()
return model, stride, names, pt, jit, onnx, engine
def run(model, img, stride, pt,
imgsz=(640, 640), # inference size (height, width)
conf_thres=0.15, # confidence threshold
iou_thres=0.15, # NMS IOU threshold
max_det=1000, # maximum detections per image
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu
classes=None, # filter by class: --class 0, or --class 0 2 3
agnostic_nms=False, # class-agnostic NMS
augment=False, # augmented inference
half=False, # use FP16 half-precision inference
):
cal_detect = []
device = select_device(device)
names = model.module.names if hasattr(model, 'module') else model.names # get class names
# Set Dataloader
im = letterbox(img, imgsz, stride, pt)[0]
# Convert
im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
im = np.ascontiguousarray(im)
im = torch.from_numpy(im).to(device)
im = im.half() if half else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
pred = model(im, augment=augment)
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
# Process detections
for i, det in enumerate(pred): # detections per image
if len(det):
# Rescale boxes from img_size to im0 size
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], img.shape).round()
# Write results
for *xyxy, conf, cls in reversed(det):
c = int(cls) # integer class
label = f'{names[c]}'
lbl = names[int(cls)]
#print(lbl)
#if lbl not in [' Chef clothes',' clothes']:
#continue
cal_detect.append([label, xyxy,str(float(conf))[:5]])
return cal_detect
def det_yolov5v6(info1):
global model, stride, names, pt, jit, onnx, engine
if info1[-3:] in ['jpg','png','jpeg','tif','bmp']:
image = cv2.imread(info1) # 读取识别对象
#try:
results = run(model, image, stride, pt) # 识别, 返回多个数组每个第一个为结果,第二个为坐标位置
for i in results:
box = i[1]
p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3]))
color = [0,0,255]
ui.printf(str(time.strftime('%Y.%m.%d %H:%M:%S ', time.localtime(time.time()))) + '检测到' + str(i[0]) + ' 种类的木材缺陷')
cv2.rectangle(image, p1, p2, color, thickness=3, lineType=cv2.LINE_AA)
cv2.putText(image, str(i[0]) + ' ' + str(i[2])[:5], (int(box[0]), int(box[1]) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.75, color, 2)
#except:
#pass
ui.showimg(image)
QApplication.processEvents()
class Thread_1(QThread): # 线程1
def __init__(self,info1):
super().__init__()
self.info1=info1
self.run2(self.info1)
def run2(self, info1):
result = []
result = det_yolov5v6(info1)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1280, 960)
MainWindow.setStyleSheet("background-image: url(\"./template/carui.png\")")
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(168, 60, 501, 71))
self.label.setAutoFillBackground(False)
self.label.setStyleSheet("")
self.label.setFrameShadow(QtWidgets.QFrame.Plain)
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setObjectName("label")
self.label.setStyleSheet("font-size:42px;font-weight:bold;font-family:SimHei;background:rgba(255,255,255,0.8);")
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setGeometry(QtCore.QRect(40, 188, 751, 501))
self.label_2.setStyleSheet("background:rgba(255,255,255,0.3);")
self.label_2.setAlignment(QtCore.Qt.AlignCenter)
self.label_2.setObjectName("label_2")
self.textBrowser = QtWidgets.QTextBrowser(self.centralwidget)
self.textBrowser.setGeometry(QtCore.QRect(73, 746, 851, 174))
self.textBrowser.setStyleSheet("background:rgba(255,255,255,0.7);")
self.textBrowser.setObjectName("textBrowser")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(1020, 750, 150, 40))
self.pushButton.setStyleSheet("background:rgba(0,142,255,1);border-radius:10px;padding:2px 4px;")
self.pushButton.setObjectName("pushButton")
self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
self.pushButton_2.setGeometry(QtCore.QRect(1020, 800, 150, 40))
......
该程序文件名为ui.py,主要功能是实现一个木材缺陷检测系统的图形界面。程序导入了多个库,包括argparse、platform、shutil、time、numpy、cv2、torch等。程序定义了一些函数,包括load_model()、run()、det_yolov5v6()等。其中load_model()函数用于加载模型,run()函数用于运行模型进行目标检测,det_yolov5v6()函数用于进行YOLOv5v6目标检测。程序还定义了一个线程类Thread_1,用于在后台运行目标检测算法。图形界面使用了PyQt5库进行设计,包括一个主窗口和一些按钮、标签等控件。
class WassersteinLoss:
def __init__(self, eps=1e-7, constant=12.8):
self.eps = eps
self.constant = constant
def __call__(self, pred, target):
center1 = pred[:, :2]
center2 = target[:, :2]
whs = center1[:, :2] - center2[:, :2]
center_distance = whs[:, 0] * whs[:, 0] + whs[:, 1] * whs[:, 1] + self.eps
w1 = pred[:, 2] + self.eps
h1 = pred[:, 3] + self.eps
w2 = target[:, 2] + self.eps
h2 = target[:, 3] + self.eps
wh_distance = ((w1 - w2) ** 2 + (h1 - h2) ** 2) / 4
wasserstein_2 = center_distance + wh_distance
return torch.exp(-torch.sqrt(wasserstein_2) / self.constant)
这个程序文件名为yolov5-NWD.py,主要实现了一个名为wasserstein_loss的函数,用于计算目标检测中的损失函数。该函数是根据论文《Enhancing Geometric Factors into Model Learning and Inference for Object Detection and Instance Segmentation》进行实现的,代码修改自https://github.com/Zzh-tju/CIoU。
该函数接受两个参数pred和target,分别表示预测的边界框和真实的边界框。函数首先根据预测和真实边界框的中心点计算中心距离,然后计算边界框的宽高距离。接着根据中心距离和宽高距离计算Wasserstein距离。最后,通过指数函数和常数constant对Wasserstein距离进行处理,得到最终的损失值。
在主程序中,首先调用wasserstein_loss函数计算损失值nwd。然后根据给定的iou_ratio参数,计算最终的lbox损失值,其中包括(1 - iou_ratio) * (1.0 - nwd).mean()和iou_ratio * (1.0 - iou).mean()两部分。最后,根据计算得到的iou和nwd值,进行一些处理并限制在0到1之间,得到最终的iou值。
class Conv(nn.Module):
# Standard convolution
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
return self.act(self.conv(x))
class DWConv(Conv):
# Depth-wise convolution class
def __init__(self, c1, c2, k=1, s=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), act=act)
class TransformerLayer(nn.Module):
# Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance)
def __init__(self, c, num_heads):
super().__init__()
self.q = nn.Linear(c, c, bias=False)
self.k = nn.Linear(c, c, bias=False)
self.v = nn.Linear(c, c, bias=False)
self.ma = nn.MultiheadAttention(embed_dim=c, num_heads=num_heads)
self.fc1 = nn.Linear(c, c, bias=False)
self.fc2 = nn.Linear(c, c, bias=False)
def forward(self, x):
x = self.ma(self.q(x), self.k(x), self.v(x))[0] + x
x = self.fc2(self.fc1(x)) + x
return x
class TransformerBlock(nn.Module):
# Vision Transformer https://arxiv.org/abs/2010.11929
def __init__(self, c1, c2, num_heads, num_layers):
super().__init__()
self.conv = None
if c1 != c2:
self.conv = Conv(c1, c2)
self.linear = nn.Linear(c2, c2) # learnable position embedding
self.tr = nn.Sequential(*(TransformerLayer(c2, num_heads) for _ in range(num_layers)))
self.c2 = c2
def forward(self, x):
if self.conv is not None:
x = self.conv(x)
b, _, w, h = x.shape
p = x.flatten(2).permute(2, 0, 1)
return self.tr(p + self.linear(p)).permute(1, 2, 0).reshape(b, self.c2, w, h)
class Bottleneck(nn.Module):
# Standard bottleneck
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_, c2, 3, 1, g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class BottleneckCSP(nn.Module):
# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
self.cv4 = Conv(2 * c_, c2, 1, 1)
self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3)
self.act = nn.SiLU()
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
def forward(self, x):
y1 = self.cv3(self.m(self.cv1(x)))
y2 = self.cv2(x)
return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))
class C3(nn.Module):
# CSP Bottleneck with 3 convolutions
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2)
self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
# self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)])
def forward(self, x):
return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
class C3TR(C3):
# C3 module with TransformerBlock()
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
super().__init__(c1, c2, n, shortcut, g, e)
c_ = int(c2 * e)
self.m = TransformerBlock(c_, c_, 4, n)
class C3SPP(C3):
# C3 module with SPP()
def __init__(self, c1, c2, k=(
这个程序文件是YOLOv5的一个模块,包含了一些常用的函数和类。文件中定义了一些卷积和池化层的类,以及一些常用的操作函数。这些类和函数被用于构建YOLOv5的网络结构。
class CrossConv(nn.Module):
# Cross Convolution Downsample
def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False):
# ch_in, ch_out, kernel, stride, groups, expansion, shortcut
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, (1, k), (1, s))
self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
class Sum(nn.Module):
# Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070
def __init__(self, n, weight=False): # n: number of inputs
super().__init__()
self.weight = weight # apply weights boolean
self.iter = range(n - 1) # iter object
if weight:
self.w = nn.Parameter(-torch.arange(1.0, n) / 2, requires_grad=True) # layer weights
def forward(self, x):
y = x[0] # no weight
if self.weight:
w = torch.sigmoid(self.w) * 2
for i in self.iter:
y = y + x[i + 1] * w[i]
else:
for i in self.iter:
y = y + x[i + 1]
return y
class MixConv2d(nn.Module):
# Mixed Depth-wise Conv https://arxiv.org/abs/1907.09595
def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True): # ch_in, ch_out, kernel, stride, ch_strategy
super().__init__()
n = len(k) # number of convolutions
if equal_ch: # equal c_ per group
i = torch.linspace(0, n - 1E-6, c2).floor() # c2 indices
c_ = [(i == g).sum() for g in range(n)] # intermediate channels
else: # equal weight.numel() per group
b = [c2] + [0] * n
a = np.eye(n + 1, n, k=-1)
a -= np.roll(a, 1, axis=1)
a *= np.array(k) ** 2
a[0] = 1
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b
self.m = nn.ModuleList(
[nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)])
self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU()
def forward(self, x):
return self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
class Ensemble(nn.ModuleList):
# Ensemble of models
def __init__(self):
super().__init__()
def forward(self, x, augment=False, profile=False, visualize=False):
y = []
for module in self:
y.append(module(x, augment, profile, visualize)[0])
# y = torch.stack(y).max(0)[0] # max ensemble
# y = torch.stack(y).mean(0) # mean ensemble
y = torch.cat(y, 1) # nms ensemble
return y, None # inference, train output
def attempt_load(weights, map_location=None, inplace=True, fuse=True):
from models.yolo import Detect, Model
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
model = Ensemble()
for w in weights if isinstance(weights, list) else [weights]:
ckpt = torch.load(attempt_download(w), map_location=map_location) # load
if fuse:
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model
else:
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().eval()) # without layer fuse
# Compatibility updates
for m in model.modules():
if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model]:
m.inplace = inplace # pytorch 1.7.0 compatibility
if type(m) is Detect:
if not isinstance(m.anchor_grid, list): # new Detect Layer compatibility
delattr(m, 'anchor_grid')
setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl)
elif type(m) is Conv:
m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility
if len(model) == 1:
return model[-1] # return model
else:
print(f'Ensemble created with {weights}\n')
for k in ['names']:
setattr(model, k, getattr(model[-1], k))
model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride
return model # return ensemble
这个程序文件是YOLOv5的实验模块。文件中定义了几个类,包括CrossConv、Sum、MixConv2d和Ensemble。
CrossConv类是一个跨通道卷积下采样模块,它接受输入张量x,并通过两个卷积层进行处理,然后将结果与输入张量相加,最后返回结果。
Sum类是一个加权求和模块,它接受多个输入张量x,并根据权重对它们进行加权求和,最后返回结果。
MixConv2d类是一个混合深度卷积模块,它接受输入张量x,并通过多个卷积层进行处理,然后将结果进行拼接,最后返回结果。
Ensemble类是一个模型集合,它继承自nn.ModuleList,并重写了forward方法,用于对输入张量x进行多个模型的推理,并将结果进行拼接返回。
此外,文件还定义了一个辅助函数attempt_load,用于加载模型权重。它接受一个权重路径或路径列表作为输入,并返回一个模型或模型集合。
总体来说,这个程序文件实现了YOLOv5的一些实验模块,并提供了加载模型权重的功能。
根据以上分析,该程序是一个用于木材缺陷检测的系统。它基于YOLOv5模型,并融合了NWD_loss损失函数。系统包含了训练、推理和图形界面等功能。
整体构架如下:
train.py:用于训练YOLOv5模型的脚本,支持单GPU和多GPU分布式训练。
ui.py:实现了木材缺陷检测系统的图形界面,使用PyQt5库进行设计。
loss.py:定义了多个损失函数类,用于计算损失函数。
yolov5-NWD.py:实现了计算目标检测中的损失函数的wasserstein_loss函数。
models\common.py:包含了一些常用的卷积和池化层的类,用于构建YOLOv5的网络结构。
models\experimental.py:定义了一些实验模块,包括跨通道卷积下采样模块、加权求和模块和混合深度卷积模块。
models\tf.py:包含了一些与TensorFlow相关的函数和类。
models\yolo.py:定义了YOLOv5的网络结构。
models_init_.py:模型初始化文件。
tools目录:包含了一些工具函数和类,用于数据处理、模型加载、优化器设置、学习率调整等操作。
utils目录:包含了一些辅助函数和类,用于数据处理、模型加载、优化器设置、学习率调整等操作。
tools\aws目录:包含了与AWS相关的函数和类。
tools\flask_rest_api目录:包含了用于搭建Flask REST API的函数和类。
tools\loggers目录:包含了日志记录器相关的函数和类。
tools\loggers\wandb目录:包含了与WandB日志记录器相关的函数和类。
utils\aws目录:包含了与AWS相关的函数和类。
utils\flask_rest_api目录:包含了用于搭建Flask REST API的函数和类。
utils\loggers目录:包含了日志记录器相关的函数和类。
utils\loggers\wandb目录:包含了与WandB日志记录器相关的函数和类。
下面是每个文件的功能整理:
文件路径 | 功能 |
---|---|
loss.py | 计算损失函数 |
train.py | 训练YOLOv5模型 |
ui.py | 实现木材缺陷检测系统的图形界面 |
yolov5-NWD.py | 计算目标检测中的损失函数 |
models\common.py | 定义常用的卷积和池化层的类 |
models\experimental.py | 定义实验模块 |
models\tf.py | 包含与TensorFlow相关的函数和类 |
models\yolo.py | 定义YOLOv5的网络结构 |
models_init_.py | 模型初始化文件 |
tools\activations.py | 激活函数相关的函数和类 |
tools\augmentations.py | 数据增强相关的函数和类 |
tools\autoanchor.py | 自动锚框相关的函数和类 |
tools\autobatch.py | 自动批处理相关的函数和类 |
tools\callbacks.py | 回调函数相关的函数和类 |
tools\datasets.py | 数据集相关的函数和类 |
tools\downloads.py | 下载相关的函数和类 |
tools\general.py | 通用函数和类 |
tools\loss.py | 损失函数相关的函数和类 |
tools\metrics.py | 评估指标相关的函数和类 |
tools\plots.py | 绘图相关的函数和类 |
tools\torch_utils.py | PyTorch工具函数和类 |
tools_init_.py | 工具初始化文件 |
tools\aws\resume.py | AWS恢复相关的函数和类 |
tools\aws_init_.py | AWS初始化文件 |
tools\flask_rest_api\example_request.py | Flask REST API示例请求 |
tools\flask_rest_api\restapi.py | Flask REST API相关的函数和类 |
tools\loggers_init_.py | 日志记录器初始化文件 |
tools\loggers\wandb\log_dataset.py | WandB日志记录器的数据集日志记录 |
tools\loggers\wandb\sweep.py | WandB日志记录器的超参数搜索 |
tools\loggers\wandb\wandb_utils.py | WandB日志记录器的工具函数 |
tools\loggers\wandb_init_.py | WandB日志记录器初始化文件 |
utils\activations.py | 激活函数相关的函数和类 |
utils\augmentations.py | 数据增强相关的函数和类 |
utils\autoanchor.py | 自动锚框相关的函数和类 |
utils\autobatch.py | 自动批处理相关的函数和类 |
utils\callbacks.py | 回调函数相关的函数和类 |
utils\datasets.py | 数据集相关的函数和类 |
utils\downloads.py | 下载相关的函数和类 |
utils\general.py | 通用函数和类 |
utils\loss.py | 损失函数相关的函数和类 |
utils\metrics.py | 评估指标相关的函数和类 |
utils\plots.py | 绘图相关的函数和类 |
utils\torch_utils.py | PyTorch工具函数和类 |
utils_init_.py | 辅助函数初始化文件 |
utils\aws\resume.py | AWS恢复相关的函数和类 |
utils\aws_init_.py | AWS初始化文件 |
utils\flask_rest_api\example_request.py | Flask REST API示例请求 |
utils\flask_rest_api\restapi.py | Flask REST API相关的函数和类 |
utils\loggers_init_.py | 日志记录器初始化文件 |
utils\loggers\wandb\log_dataset.py | WandB日志记录器的数据集日志记录 |
utils\loggers\wandb\sweep.py | WandB日志记录器的超参数搜索 |
utils\loggers\wandb\wandb_utils.py | WandB日志记录器的工具函数 |
utils\loggers\wandb_init_.py | WandB日志记录器初始化文件 |
小目标检测以往的小目标检测方法大致可以分为3大类:
(1)多尺度特征学习
(2)设计更好的训练策略
(3)基于GAN增强的检测
一种简单而经典的方法是将输入图像的大小调整为不同的尺度,并训练不同的检测器,每一个检测器都能在一定的尺度范围内达到最佳性能。为了降低计算成本,一些研究尝试构建不同尺度的特征级金字塔。例如,SSD从不同分辨率的特征图中检测目标。特征金字塔网络(Feature Pyramid Network, FPN)采用横向连接的自顶向下结构,将不同尺度的特征信息结合起来,提高目标检测性能。在此基础上,提出了进一步提高FPN性能的方法,包括PANet、BiFPN、Recursive-FPN。此外,TridentNet构建了具有不同感受野的并行多分支体系结构,以生成特定比例的特征图。
Singh等人受到同时检测小目标和大目标很难的观察启发,提出了SNIP和SNIPER在一定规模范围内选择性训练目标。此外,Kim等人引入了Scale-Aware网络(SAN),并将从不同空间提取的特征映射到一个尺度不变的子空间,使检测器对尺度变化具有更强的鲁棒性。
Perceptual GAN是第一个尝试将GAN应用于小目标检测的算法,它通过缩小小目标与大目标的表示差异来改进小目标检测。此外,Bai等人提出了一种MT-GAN来训练图像级超分辨率模型,以增强小ROI的特征。此外,有研究提出了一种特征超分辨率方法来提高基于建议检测器的小目标检测性能。
IoU是度量边界框之间相似性的最广泛使用的度量方法。然而,IoU只能在边界框有重叠情况下的问题。为了解决这一问题,提出了一种Generalized IoU (GIoU)的方法,该方法通过最小外接边界框相关的惩罚项来实现。然而,当一个边界框包含另一个边界框时,GIoU将降级为IoU。因此,为了克服IoU和GIoU的局限性提出了DIoU和CIoU,它们考虑了重叠面积、中心点距离和纵横比这三个几何特性。
GIoU、CIoU和DIoU主要应用于NMS和loss function中代替IoU以提高总体目标检测性能,但在标签分配中的应用很少讨论。在相似工作中,Yang等人也提出了Gaussian Wasserstein Distance (GWD)损失用于Oriented目标检测,通过测量Oriented BBox的位置关系。然而,该方法的目的是解决Oriented目标检测中的边界不连续和square-like问题。本文的动机是为了减轻IoU对小目标位置偏差的敏感性,本文提出的方法可以在Anchor-Based的目标检测中取代IoU。
将高质量的Anchor分配到GT小目标Box中是一项具有挑战性的任务。一个简单的方法是在选择正样本时降低IoU阈值。虽然可以使小目标匹配更多的Anchor,但训练样本的整体质量会下降。此外,最近的许多研究都试图使标签分配过程更具自适应性,以提高检测性能。例如,Zhang等人提出了自适应训练样本选择(Adaptive Training Sample Selection, ATSS),通过一组Anchor的IoU统计值自动计算每个GT的Pos/Neg阈值。Kang等人通过假设Pos/Neg的联合损失分布服从高斯分布,引入了概率Anchor Assignment (PAA)。此外,Optimal Transport Assignment (OTA)将标签分配过程作为一个全局视角的最优运输问题。但这些方法都是利用IoU度量来度量2个BBox之间的相似性,主要关注标签分配中的阈值设置,不适合TOD。相比之下,本文的研究重点是设计一种更好的评价指标,用以替代小目标检测中的IoU指标。
使用Optimal Transport理论中的Wasserstein distance来计算分布距离。对于2个二维高斯分布,和,和之间的Wasserstein distance为:
上式可以简化为:
其中,是Frobenius norm。
此外,对于由BBox 和建模的高斯分布和,上式可进一步简化为:
但是是一个距离度量,不能直接用作相似性度量(即0-1之间的值作为IoU)。因此,使用它的指数形式归一化,得到了新的度量,称为Normalized Wasserstein Distance(NWD):
其中C是与数据集密切相关的常数。在接下来的实验中,设置C到AI-TOD的平均绝对大小并达到最佳性能。此外,观察到C在一定范围内是稳健的,细节将在补充材料中显示。
与IoU相比,NWD在检测小目标方面具有以下优点:
尺度不确定性;
位置偏差平滑性;
测量非重叠或相互包容的边界盒之间的相似性。
如图所示,在不失通用性的前提下,在以下2种情况下讨论度量值的变化。
在图2的第1行中,保持Box A和Box B的尺度相同,而将Box B沿A的对角线移动。可以看出,这4条NWD曲线完全重合,说明NWD对Box的尺度方差不敏感。此外,可以观察到IoU对微小的位置偏差过于敏感,而位置偏差导致的NWD变化更为平滑。对位置偏差的平滑性表明,在相同阈值下,Pos/Neg样本之间可能比IoU有更好的区分。
在图2的第2行中,在B的边长一半位置延对角线方法A,与IoU相比,NWD的曲线更加平滑,能够一致地反映A与B之间的相似性。
提出的NWD可以很容易地集成到任何Anchor-Based Detectors,以取代IoU。在不失一般性的前提下,本文采用了具有代表性的基于Anchor的Faster R-CNN来描述的NWD用法。
具体来说,所有的修改都是在IoU最初使用的3个部分进行的,包括pos/neg label assignment, NMS和Regression loss function。
具体内容如下:
1、NWD-based Label Assignment
Faster R-CNN由2个网络组成:
用于生成区域建议的RPN
基于区域建议检测目标的R-CNN
RPN和R-CNN都包含标签分配过程。
对于RPN,首先生成不同尺度和比例的Anchor,然后给Anchor分配二值标签,训练分类和回归头。
对于R-CNN,标签分配过程与RPN相似,不同之处在于R-CNN的输入就是RPN的输出。
为了克服IoU在小目标检测中的上述缺点,设计了基于NWD的标签分配策略,利用NWD来分配标签。
具体来说,训练的RPN,positive标签将被分配到2种类型的Anchor:
The anchor with the highest NWD value with a gt box and the NWD value is larger than θ;
The anchor that has the NWD value higher than the positive threshold θ with any gt 。
因此,如果Anchor的NWD值低于负阈值θ(所有gt Box),则将给Anchor分配负标签。此外,既没有被分配正标签也没有被分配负标签的Anchor不参与训练过程。需要注意的是,为了将NWD直接应用到Anchor-Based检测器中,实验中使用了原始检测器的θ和θ。
2、NWD-based NMS
NMS是目标检测中不可或缺的一部分,用于抑制冗余预测边界框,其中应用了IoU度量。首先,它根据得分对所有预测框进行排序。选择得分最高的预测框M,并抑制与M有显著重叠(使用预定义的阈值Nt)的所有其他预测框。这个过程递归地应用于其余的框。但是,IoU对小目标的敏感性会使许多预测框的IoU值低于Nt,从而导致假阳性预测。
为了解决这一问题,作者认为NWD在小目标检测中是一个更好的NMS标准,因为NWD克服了尺度敏感性问题。此外,只需要几个代码,基于NWD的NMS就可以灵活地集成到任何小目标检测器。
3、NWD-based Regression Loss
IoU-Loss的引入是为了消除训练和测试之间的性能差距。然而,在以下2种情况下IoU-Loss不能提供梯度优化网络:
预测框与GT框之间没有重叠边界框(即)
预测框与GT框呈现包含关系。
此外,这2种情况对小目标是非常普遍的。具体来说,一方面几个像素P的偏差将导致P和G之间没有重叠;另一方面,小目标很容易被错误的预测导致 。因此,IoU-Loss不适合小目标检测器。
CIoU和DIoU虽然可以处理以上2种情况,但由于它们都是基于IoU的,所以对小目标的位置偏差非常敏感。为解决上述问题,作者将NWD指标设计为损失函数:
其中,为预测框p的高斯分布模型,为GT Box G的高斯分布模型。根据介绍,即使在的情况下,基于NWD的损失也可以提供梯度。
在木材缺陷检测领域,小目标的检测一直是一个挑战。现有的物体检测器对于小目标的检测效果有限,因为缺乏足够的信息量。NWD作为一种新的度量方式,在解决小目标检测中的IoU敏感性问题方面显示出了潜力。在我们的研究中,我们将介绍如何融合NWD_loss到YOLOv5中,以提升木材缺陷检测系统的性能。
标签分配和样本选择
NWD的优势在于它对目标尺度不敏感且稳定,解决了IoU在小目标上的敏感性问题。我们重新设计了样本选择策略,在训练YOLOv5时利用NWD度量方式对正负样本进行分配,确保每个样本都能提供足够的监督信息,尤其是针对小目标。
损失函数改进
在YOLOv5的损失函数中,我们替代了传统的IoU度量方式,将NWD作为新的度量方式。这种改进有效地提高了网络对小目标的检测精度,在小目标的位置变化时保持相对稳定的损失,有助于网络更好地收敛。
NMS替代
传统的NMS(Non-Maximum Suppression)也采用了IoU度量来进行边界框的筛选,但我们将其替代为NWD,在后处理阶段保持了对小目标的更准确的筛选和融合。
训练损失( train/box_loss, train/obj_loss, train/cls_loss):这些值表示模型在训练过程中对不同任务(例如边界框检测、目标检测、类别识别)的损失。
评价指标( metrics/precision, metrics/recall, metrics/mAP_0.5, metrics/mAP_0.5:0.95):这些指标分别代表准确率、识别率以及在不同IoU(Intersection over Union)阈值下的平均精度。
验证损失( val/box_loss, val/obj_loss, val/cls_loss):这些值表示模型在验证集上的表现。
学习率( x/lr0, x/lr1, x/lr2):这些值代表训练过程中的学习率变化。
为了进行深入的数据分析,我们将通过创建一系列训练图表来探索这些数据。这包括损失函数和评价指标的趋势图,以及学习率的变化。通过这些图表,我们可以更好地理解模型的过程中的表现和学习动态。
# 设置绘图风格
sns.set(style="whitegrid")
# 创建一个图表,显示训练和验证损失
plt.figure(figsize=(15, 10))
# 绘制训练损失
plt.subplot(2, 2, 1)
plt.plot(data['epoch'], data['train/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['train/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['train/cls_loss'], label='Classification Loss')
plt.title('Training Losses')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# 绘制验证损失
plt.subplot(2, 2, 2)
plt.plot(data['epoch'], data['val/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['val/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['val/cls_loss'], label='Classification Loss')
plt.title('Validation Losses')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# 绘制精确度和召回率
plt.subplot(2, 2, 3)
plt.plot(data['epoch'], data['metrics/precision'], label='Precision')
plt.plot(data['epoch'], data['metrics/recall'], label='Recall')
plt.title('Precision and Recall')
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend()
# 绘制平均精度
plt.subplot(2, 2, 4)
plt.plot(data['epoch'], data['metrics/mAP_0.5'], label='mAP 0.5')
plt.plot(data['epoch'], data['metrics/mAP_0.5:0.95'], label='mAP 0.5:0.95')
plt.title('Mean Average Precision (mAP)')
plt.xlabel('Epoch')
plt.ylabel('mAP')
plt.legend()
plt.tight_layout()
plt.show()
训练损失:从训练损失来看,随着训练周期的增加,box_loss、obj_loss和cls_loss均呈下降趋势,这表明模型在学习过程中不断改善其在不同任务上的表现。
验证损失:验证损失的趋势与训练相似损失,同样显示出随着训练的进行而逐渐减少。这表明模型对未见数据的泛化能力在提升。
准确度和识别率:准确度和识别率在训练过程中波动,但总体呈上升趋势。这意味着模型越来越能够准确识别木材缺陷,并且在检测到的缺陷中,真正缺陷的比例同时增加。
平均精度(mAP):mAP 0.5 和 mAP 0.5:0.95 都随着训练周期的增加而上升。mAP 0.5:0.95 的提升急剧显着,这表示模型在更严格的 IoU 阈值下的表现同时提高。
总的来说,这些图表表明模型在训练过程中不断提升其性能,尤其是在木材缺陷的检测方面。损失函数的下降、准确度和识别率的提高,以及mAP的增加都指示了模型的有效然而,也应该注意准确度和识别率的波动,这可能表明模型在某些训练阶段可能面临过度优化或其他优化的挑战。通过进一步的调整参与和优化,可以在这些方面期待模型的进一步改善。
下图完整源码&数据集&环境部署视频教程&自定义UI界面
参考博客《【小目标SOTA】融合NWD_loss的YOLO的木材缺陷检测系统》
[1]许德刚,王露,李凡.深度学习的典型目标检测算法研究综述[J].计算机工程与应用.2021,(8).DOI:10.3778/j.issn.1002-8331.2012-0449 .
[2]李丽萍,王君超,苏治钦.石材粗加工锯切机的设计与分析[J].制造技术与机床.2021,(5).DOI:10.19287/j.cnki.1005-2402.2021.05.023 .
[3]王舒,韩慧平.劳动力成本变化对我国家具产业价值链的影响研究[J].中国林业经济.2021,(3).DOI:10.13691/j.cnki.cn23-1539/f.2021.03.019 .
[4]孟祥泽.基于深度卷积神经网络的图像目标检测算法现状研究综述[J].数字技术与应用.2021,(1).DOI:10.19695/j.cnki.cn12-1369.2021.01.35 .
[5]任长清,娄月轩,杨春梅,等.基于PLC的小径木圆锯机进出料台控制系统研究[J].现代电子技术.2021,(1).DOI:10.16652/j.issn.1004-373x.2021.01.027 .
[6]黄虎,王青云,周琦.基于YOLO v3的变电站作业安全帽佩戴识别[J].南京工程学院学报(自然科学版).2020,(3).DOI:10.13960/j.issn.1672-2558.2020.03.008 .
[7]陈哲,尚凯,张青,等.基于TOPSIS-PSI方法的办公座椅设计评价[J].林业工程学报.2020,(6).DOI:10.13360/j.issn.2096-1359.201912003 .
[8]冷伟,郑鼎聪,周建方,等.改进的模糊层次分析法在水工钢闸门可靠性分配中的应用[J].水利与建筑工程学报.2020,(6).DOI:10.3969/j.issn.1672-1144.2020.06.004 .
[9]田振成,王继荣,王士华,等.基于BWM与改进熵-TOPSIS法的PCB自动装壳生产线方案评价[J].青岛大学学报(自然科学版).2020,(2).DOI:10.3969/j.issn.1006-1037.2020.05.09 .
[10]崔海鸥,刘珉.我国第九次森林资源清查中的资源动态研究[J].西部林业科学.2020,(5).DOI:10.16473/j.cnki.xblykx1972.2020.05.014 .