【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统

1.研究背景与意义

项目参考AAAI Association for the Advancement of Artificial Intelligence

研究背景与意义

目标检测是计算机视觉领域的一个重要研究方向,它在许多应用中发挥着关键作用,如智能监控、自动驾驶、人脸识别等。目标检测的目标是在图像或视频中准确地定位和识别出感兴趣的目标物体。随着深度学习的发展,基于卷积神经网络(CNN)的目标检测算法取得了显著的进展。

其中,YOLO(You Only Look Once)是一种非常流行的实时目标检测算法。YOLO算法通过将目标检测问题转化为一个回归问题,将图像分割为多个网格,并为每个网格预测目标的边界框和类别概率。然而,传统的YOLO算法存在一些问题,如对小目标的检测效果较差、定位不准确等。

为了解决这些问题,研究者们提出了YOLOv2、YOLOv3等改进版本。这些改进版本通过引入多尺度特征融合的方法,显著提高了目标检测的性能。然而,这些改进版本仍然存在一些不足之处,如计算复杂度较高、对大目标的检测效果有限等。

因此,本研究旨在进一步改进YOLOv8算法,以融合多尺度特征来提高目标检测的性能。具体来说,我们计划通过以下几个方面的工作来实现目标:

首先,我们将探索如何更好地融合多尺度特征。目前的YOLOv8算法主要通过将不同层级的特征图进行简单的叠加来实现多尺度特征融合。然而,这种简单的叠加方式可能会导致信息的丢失和冗余。因此,我们将尝试引入更复杂的特征融合方法,如注意力机制、特征金字塔等,以提高特征的表达能力和判别能力。

其次,我们将研究如何更好地处理小目标和大目标。目前的YOLOv8算法在处理小目标和大目标时都存在一定的困难。对于小目标,由于其尺寸较小,容易被忽略或误判为背景。对于大目标,由于其尺寸较大,容易被分割成多个小目标。因此,我们将探索如何设计更有效的网络结构和损失函数,以提高对小目标和大目标的检测效果。

最后,我们将对改进后的算法进行大量的实验和评估。我们将使用公开的目标检测数据集,如COCO、VOC等,来评估改进后算法的性能。我们将比较改进后的算法与其他经典的目标检测算法进行对比,并分析其优势和不足之处。

通过以上工作,我们期望能够提出一种更加准确、高效的目标检测算法,为实际应用提供更好的支持。这将对智能监控、自动驾驶、人脸识别等领域的发展产生积极的影响。同时,本研究还将为目标检测算法的改进和优化提供一定的参考和借鉴。

2.图片演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.视频演示

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计_哔哩哔哩_bilibili

4.数据集的采集&标注和整理

图片的收集

首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集JLDatasets。

在这里插入图片描述

eiseg是一个图形化的图像注释工具,支持COCO和YOLO格式。以下是使用eiseg将图片标注为COCO格式的步骤:

(1)下载并安装eiseg。
(2)打开eiseg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的JSON文件。
(6)重复此过程,直到所有的图片都标注完毕。

由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。

下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。

import contextlib
import jsonimport cv2
import pandas as pd
from PIL import Image
from collections import defaultdictfrom utils import *# Convert INFOLKS JSON file into YOLO-format labels ----------------------------
def convert_infolks_json(name, files, img_path):# Create folderspath = make_dirs()# Import jsondata = []for file in glob.glob(files):with open(file) as f:jdata = json.load(f)jdata['json_file'] = filedata.append(jdata)# Write images and shapesname = path + os.sep + namefile_id, file_name, wh, cat = [], [], [], []for x in tqdm(data, desc='Files and Shapes'):f = glob.glob(img_path + Path(x['json_file']).stem + '.*')[0]file_name.append(f)wh.append(exif_size(Image.open(f)))  # (width, height)cat.extend(a['classTitle'].lower() for a in x['output']['objects'])  # categories# filenamewith open(name + '.txt', 'a') as file:file.write('%s\n' % f)# Write *.names filenames = sorted(np.unique(cat))# names.pop(names.index('Missing product'))  # removewith open(name + '.names', 'a') as file:[file.write('%s\n' % a) for a in names]# Write labels filefor i, x in enumerate(tqdm(data, desc='Annotations')):label_name = Path(file_name[i]).stem + '.txt'with open(path + '/labels/' + label_name, 'a') as file:for a in x['output']['objects']:# if a['classTitle'] == 'Missing product':#    continue  # skipcategory_id = names.index(a['classTitle'].lower())# The INFOLKS bounding box format is [x-min, y-min, x-max, y-max]box = np.array(a['points']['exterior'], dtype=np.float32).ravel()box[[0, 2]] /= wh[i][0]  # normalize x by widthbox[[1, 3]] /= wh[i][1]  # normalize y by heightbox = [box[[0, 2]].mean(), box[[1, 3]].mean(), box[2] - box[0], box[3] - box[1]]  # xywhif (box[2] > 0.) and (box[3] > 0.):  # if w > 0 and h > 0file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))# Split data into train, test, and validate filessplit_files(name, file_name)write_data_data(name + '.data', nc=len(names))print(f'Done. Output saved to {os.getcwd() + os.sep + path}')# Convert vott JSON file into YOLO-format labels -------------------------------
def convert_vott_json(name, files, img_path):# Create folderspath = make_dirs()name = path + os.sep + name# Import jsondata = []for file in glob.glob(files):with open(file) as f:jdata = json.load(f)jdata['json_file'] = filedata.append(jdata)# Get all categoriesfile_name, wh, cat = [], [], []for i, x in enumerate(tqdm(data, desc='Files and Shapes')):with contextlib.suppress(Exception):cat.extend(a['tags'][0] for a in x['regions'])  # categories# Write *.names filenames = sorted(pd.unique(cat))with open(name + '.names', 'a') as file:[file.write('%s\n' % a) for a in names]# Write labels filen1, n2 = 0, 0missing_images = []for i, x in enumerate(tqdm(data, desc='Annotations')):f = glob.glob(img_path + x['asset']['name'] + '.jpg')if len(f):f = f[0]file_name.append(f)wh = exif_size(Image.open(f))  # (width, height)n1 += 1if (len(f) > 0) and (wh[0] > 0) and (wh[1] > 0):n2 += 1# append filename to listwith open(name + '.txt', 'a') as file:file.write('%s\n' % f)# write labelsfilelabel_name = Path(f).stem + '.txt'with open(path + '/labels/' + label_name, 'a') as file:for a in x['regions']:category_id = names.index(a['tags'][0])# The INFOLKS bounding box format is [x-min, y-min, x-max, y-max]box = a['boundingBox']box = np.array([box['left'], box['top'], box['width'], box['height']]).ravel()box[[0, 2]] /= wh[0]  # normalize x by widthbox[[1, 3]] /= wh[1]  # normalize y by heightbox = [box[0] + box[2] / 2, box[1] + box[3] / 2, box[2], box[3]]  # xywhif (box[2] > 0.) and (box[3] > 0.):  # if w > 0 and h > 0file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))else:missing_images.append(x['asset']['name'])print('Attempted %g json imports, found %g images, imported %g annotations successfully' % (i, n1, n2))if len(missing_images):print('WARNING, missing images:', missing_images)# Split data into train, test, and validate filessplit_files(name, file_name)print(f'Done. Output saved to {os.getcwd() + os.sep + path}')# Convert ath JSON file into YOLO-format labels --------------------------------
def convert_ath_json(json_dir):  # dir contains json annotations and images# Create foldersdir = make_dirs()  # output directoryjsons = []for dirpath, dirnames, filenames in os.walk(json_dir):jsons.extend(os.path.join(dirpath, filename)for filename in [f for f in filenames if f.lower().endswith('.json')])# Import jsonn1, n2, n3 = 0, 0, 0missing_images, file_name = [], []for json_file in sorted(jsons):with open(json_file) as f:data = json.load(f)# # Get classes# try:#     classes = list(data['_via_attributes']['region']['class']['options'].values())  # classes# except:#     classes = list(data['_via_attributes']['region']['Class']['options'].values())  # classes# # Write *.names file# names = pd.unique(classes)  # preserves sort order# with open(dir + 'data.names', 'w') as f:#     [f.write('%s\n' % a) for a in names]# Write labels filefor x in tqdm(data['_via_img_metadata'].values(), desc=f'Processing {json_file}'):image_file = str(Path(json_file).parent / x['filename'])f = glob.glob(image_file)  # image fileif len(f):f = f[0]file_name.append(f)wh = exif_size(Image.open(f))  # (width, height)n1 += 1  # all imagesif len(f) > 0 and wh[0] > 0 and wh[1] > 0:label_file = dir + 'labels/' + Path(f).stem + '.txt'nlabels = 0try:with open(label_file, 'a') as file:  # write labelsfile# try:#     category_id = int(a['region_attributes']['class'])# except:#     category_id = int(a['region_attributes']['Class'])category_id = 0  # single-classfor a in x['regions']:# bounding box format is [x-min, y-min, x-max, y-max]box = a['shape_attributes']box = np.array([box['x'], box['y'], box['width'], box['height']],dtype=np.float32).ravel()box[[0, 2]] /= wh[0]  # normalize x by widthbox[[1, 3]] /= wh[1]  # normalize y by heightbox = [box[0] + box[2] / 2, box[1] + box[3] / 2, box[2],box[3]]  # xywh (left-top to center x-y)if box[2] > 0. and box[3] > 0.:  # if w > 0 and h > 0file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))n3 += 1nlabels += 1if nlabels == 0:  # remove non-labelled images from datasetos.system(f'rm {label_file}')# print('no labels for %s' % f)continue  # next file# write imageimg_size = 4096  # resize to maximumimg = cv2.imread(f)  # BGRassert img is not None, 'Image Not Found ' + fr = img_size / max(img.shape)  # size ratioif r < 1:  # downsize if necessaryh, w, _ = img.shapeimg = cv2.resize(img, (int(w * r), int(h * r)), interpolation=cv2.INTER_AREA)ifile = dir + 'images/' + Path(f).nameif cv2.imwrite(ifile, img):  # if success append image to listwith open(dir + 'data.txt', 'a') as file:file.write('%s\n' % ifile)n2 += 1  # correct imagesexcept Exception:os.system(f'rm {label_file}')print(f'problem with {f}')else:missing_images.append(image_file)nm = len(missing_images)  # number missingprint('\nFound %g JSONs with %g labels over %g images. Found %g images, labelled %g images successfully' %(len(jsons), n3, n1, n1 - nm, n2))if len(missing_images):print('WARNING, missing images:', missing_images)# Write *.names filenames = ['knife']  # preserves sort orderwith open(dir + 'data.names', 'w') as f:[f.write('%s\n' % a) for a in names]# Split data into train, test, and validate filessplit_rows_simple(dir + 'data.txt')write_data_data(dir + 'data.data', nc=1)print(f'Done. Output saved to {Path(dir).absolute()}')def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91to80=False):save_dir = make_dirs()  # output directorycoco80 = coco91_to_coco80_class()# Import jsonfor json_file in sorted(Path(json_dir).resolve().glob('*.json')):fn = Path(save_dir) / 'labels' / json_file.stem.replace('instances_', '')  # folder namefn.mkdir()with open(json_file) as f:data = json.load(f)# Create image dictimages = {'%g' % x['id']: x for x in data['images']}# Create image-annotations dictimgToAnns = defaultdict(list)for ann in data['annotations']:imgToAnns[ann['image_id']].append(ann)# Write labels filefor img_id, anns in tqdm(imgToAnns.items(), desc=f'Annotations {json_file}'):img = images['%g' % img_id]h, w, f = img['height'], img['width'], img['file_name']bboxes = []segments = []for ann in anns:if ann['iscrowd']:continue# The COCO box format is [top left x, top left y, width, height]box = np.array(ann['bbox'], dtype=np.float64)box[:2] += box[2:] / 2  # xy top-left corner to centerbox[[0, 2]] /= w  # normalize xbox[[1, 3]] /= h  # normalize yif box[2] <= 0 or box[3] <= 0:  # if w <= 0 and h <= 0continuecls = coco80[ann['category_id'] - 1] if cls91to80 else ann['category_id'] - 1  # classbox = [cls] + box.tolist()if box not in bboxes:bboxes.append(box)# Segmentsif use_segments:if len(ann['segmentation']) > 1:s = merge_multi_segment(ann['segmentation'])s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist()else:s = [j for i in ann['segmentation'] for j in i]  # all segments concatenateds = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()s = [cls] + sif s not in segments:segments.append(s)# Writewith open((fn / f).with_suffix('.txt'), 'a') as file:for i in range(len(bboxes)):line = *(segments[i] if use_segments else bboxes[i]),  # cls, box or segmentsfile.write(('%g ' * len(line)).rstrip() % line + '\n')def min_index(arr1, arr2):"""Find a pair of indexes with the shortest distance. Args:arr1: (N, 2).arr2: (M, 2).Return:a pair of indexes(tuple)."""dis = ((arr1[:, None, :] - arr2[None, :, :]) ** 2).sum(-1)return np.unravel_index(np.argmin(dis, axis=None), dis.shape)def merge_multi_segment(segments):"""Merge multi segments to one list.Find the coordinates with min distance between each segment,then connect these coordinates with one thin line to merge all segments into one.Args:segments(List(List)): original segmentations in coco's json file.like [segmentation1, segmentation2,...], each segmentation is a list of coordinates."""s = []segments = [np.array(i).reshape(-1, 2) for i in segments]idx_list = [[] for _ in range(len(segments))]# record the indexes with min distance between each segmentfor i in range(1, len(segments)):idx1, idx2 = min_index(segments[i - 1], segments[i])idx_list[i - 1].append(idx1)idx_list[i].append(idx2)# use two round to connect all the segmentsfor k in range(2):# forward connectionif k == 0:for i, idx in enumerate(idx_list):# middle segments have two indexes# reverse the index of middle segmentsif len(idx) == 2 and idx[0] > idx[1]:idx = idx[::-1]segments[i] = segments[i][::-1, :]segments[i] = np.roll(segments[i], -idx[0], axis=0)segments[i] = np.concatenate([segments[i], segments[i][:1]])# deal with the first segment and the last oneif i in [0, len(idx_list) - 1]:s.append(segments[i])else:idx = [0, idx[1] - idx[0]]s.append(segments[i][idx[0]:idx[1] + 1])else:for i in range(len(idx_list) - 1, -1, -1):if i not in [0, len(idx_list) - 1]:idx = idx_list[i]nidx = abs(idx[1] - idx[0])s.append(segments[i][nidx:])return sdef delete_dsstore(path='../datasets'):# Delete apple .DS_store filesfrom pathlib import Pathfiles = list(Path(path).rglob('.DS_store'))print(files)for f in files:f.unlink()if __name__ == '__main__':source = 'COCO'if source == 'COCO':convert_coco_json('./annotations',  # directory with *.jsonuse_segments=True,cls91to80=True)elif source == 'infolks':  # Infolks https://infolks.info/convert_infolks_json(name='out',files='../data/sm4/json/*.json',img_path='../data/sm4/images/')elif source == 'vott':  # VoTT https://github.com/microsoft/VoTTconvert_vott_json(name='data',files='../../Downloads/athena_day/20190715/*.json',img_path='../../Downloads/athena_day/20190715/')  # images folderelif source == 'ath':  # ath formatconvert_ath_json(json_dir='../../Downloads/athena/')  # images folder# zip results# os.system('zip -r ../coco.zip ../coco')
整理数据文件夹结构

我们需要将数据集整理为以下结构:

-----datasets-----coco128-seg|-----images|   |-----train|   |-----valid|   |-----test||-----labels|   |-----train|   |-----valid|   |-----test|
模型训练
 Epoch   gpu_mem       box       obj       cls    labels  img_size1/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     mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:14<00:00,  2.87it/s]all       3395      17314      0.994      0.957      0.0957      0.0843Epoch   gpu_mem       box       obj       cls    labels  img_size2/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     mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:12<00:00,  2.95it/s]all       3395      17314      0.996      0.956      0.0957      0.0845Epoch   gpu_mem       box       obj       cls    labels  img_size3/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     mAP@.5 mAP@.5:.95: 100%|███████   | 187/213 [00:52<00:00,  4.04it/s]all       3395      17314      0.996      0.957      0.0957      0.0845

5.核心代码讲解

5.1 export.py

#### 5.2 predict.py```python
class DetectionPredictor(BasePredictor):def postprocess(self, preds, img, orig_imgs):preds = ops.non_max_suppression(preds,self.args.conf,self.args.iou,agnostic=self.args.agnostic_nms,max_det=self.args.max_det,classes=self.args.classes)if not isinstance(orig_imgs, list):orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)results = []for i, pred in enumerate(preds):orig_img = orig_imgs[i]pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)img_path = self.batch[0][i]results.append(Results(orig_img, path=img_path, names=self.model.names, boxes=pred))return results

这是一个名为predict.py的程序文件,主要用于预测基于检测模型的结果。它是Ultralytics YOLO项目的一部分,使用AGPL-3.0许可证。

该文件定义了一个名为DetectionPredictor的类,继承自BasePredictor类。它包含一个postprocess方法,用于对预测结果进行后处理,并返回一个Results对象的列表。

在postprocess方法中,首先对预测结果进行非最大抑制处理,根据设定的置信度阈值和IOU阈值进行筛选。然后,将预测框的坐标进行缩放,以适应原始图像的尺寸。最后,将原始图像、图像路径、类别名称和预测框作为参数,创建一个Results对象,并将其添加到结果列表中。

该文件还包含一个示例代码,展示了如何使用DetectionPredictor类进行预测。首先导入必要的模块和资源,然后创建一个DetectionPredictor对象,并调用predict_cli方法进行预测。

总之,这个程序文件是一个用于基于检测模型进行预测的工具,提供了方便的预测方法和结果处理功能。

5.3 train.py
from copy import copy
import numpy as np
from ultralytics.data import build_dataloader, build_yolo_dataset
from ultralytics.engine.trainer import BaseTrainer
from ultralytics.models import yolo
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import LOGGER, RANK
from ultralytics.utils.torch_utils import de_parallel, torch_distributed_zero_firstclass DetectionTrainer(BaseTrainer):def build_dataset(self, img_path, mode='train', batch=None):gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32)return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=mode == 'val', stride=gs)def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode='train'):assert mode in ['train', 'val']with torch_distributed_zero_first(rank):dataset = self.build_dataset(dataset_path, mode, batch_size)shuffle = mode == 'train'if getattr(dataset, 'rect', False) and shuffle:LOGGER.warning("WARNING ⚠️ 'rect=True' is incompatible with DataLoader shuffle, setting shuffle=False")shuffle = Falseworkers = 0return build_dataloader(dataset, batch_size, workers, shuffle, rank)def preprocess_batch(self, batch):batch['img'] = batch['img'].to(self.device, non_blocking=True).float() / 255return batchdef set_model_attributes(self):self.model.nc = self.data['nc']self.model.names = self.data['names']self.model.args = self.argsdef get_model(self, cfg=None, weights=None, verbose=True):model = DetectionModel(cfg, nc=self.data['nc'], verbose=verbose and RANK == -1)if weights:model.load(weights)return modeldef get_validator(self):self.loss_names = 'box_loss', 'cls_loss', 'dfl_loss'return yolo.detect.DetectionValidator(self.test_loader, save_dir=self.save_dir, args=copy(self.args))def label_loss_items(self, loss_items=None, prefix='train'):keys = [f'{prefix}/{x}' for x in self.loss_names]if loss_items is not None:loss_items = [round(float(x), 5) for x in loss_items]return dict(zip(keys, loss_items))else:return keysdef progress_string(self):return ('\n' + '%11s' *(4 + len(self.loss_names))) % ('Epoch', 'GPU_mem', *self.loss_names, 'Instances', 'Size')def plot_training_samples(self, batch, ni):plot_images(images=batch['img'],batch_idx=batch['batch_idx'],cls=batch['cls'].squeeze(-1),bboxes=batch['bboxes'],paths=batch['im_file'],fname=self.save_dir / f'train_batch{ni}.jpg',on_plot=self.on_plot)def plot_metrics(self):plot_results(file=self.csv, on_plot=self.on_plot)def plot_training_labels(self):boxes = np.concatenate([lb['bboxes'] for lb in self.train_loader.dataset.labels], 0)cls = np.concatenate([lb['cls'] for lb in self.train_loader.dataset.labels], 0)plot_labels(boxes, cls.squeeze(), names=self.data['names'], save_dir=self.save_dir, on_plot=self.on_plot)

这是一个用于训练基于检测模型的程序文件train.py。它使用了Ultralytics YOLO库,实现了一个继承自BaseTrainer类的DetectionTrainer类。

该程序文件的主要功能包括:

  • 构建YOLO数据集
  • 构建和返回数据加载器
  • 对图像进行预处理
  • 设置模型属性
  • 返回YOLO检测模型
  • 返回用于模型验证的DetectionValidator
  • 标记训练损失项
  • 显示训练进度
  • 绘制训练样本和标签
  • 绘制训练指标

在程序文件的主函数中,首先定义了一些参数,然后创建了一个DetectionTrainer对象,并调用其train方法进行训练。

该程序文件使用了Ultralytics YOLO库提供的一些功能,包括构建数据集、构建数据加载器、模型验证等。它还使用了一些辅助函数和工具类来处理图像、绘制图表等操作。

5.5 backbone\convnextv2.py
class LayerNorm(nn.Module):def __init__(self, normalized_shape, eps=1e-6, data_format="channels_last"):super().__init__()self.weight = nn.Parameter(torch.ones(normalized_shape))self.bias = nn.Parameter(torch.zeros(normalized_shape))self.eps = epsself.data_format = data_formatif self.data_format not in ["channels_last", "channels_first"]:raise NotImplementedError self.normalized_shape = (normalized_shape, )def forward(self, x):if self.data_format == "channels_last":return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)elif self.data_format == "channels_first":u = x.mean(1, keepdim=True)s = (x - u).pow(2).mean(1, keepdim=True)x = (x - u) / torch.sqrt(s + self.eps)x = self.weight[:, None, None] * x + self.bias[:, None, None]return xclass GRN(nn.Module):def __init__(self, dim):super().__init__()self.gamma = nn.Parameter(torch.zeros(1, 1, 1, dim))self.beta = nn.Parameter(torch.zeros(1, 1, 1, dim))def forward(self, x):Gx = torch.norm(x, p=2, dim=(1,2), keepdim=True)Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6)return self.gamma * (x * Nx) + self.beta + xclass Block(nn.Module):def __init__(self, dim, drop_path=0.):super().__init__()self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)self.norm = LayerNorm(dim, eps=1e-6)self.pwconv1 = nn.Linear(dim, 4 * dim)self.act = nn.GELU()self.grn = GRN(4 * dim)self.pwconv2 = nn.Linear(4 * dim, dim)self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()def forward(self, x):input = xx = self.dwconv(x)x = x.permute(0, 2, 3, 1)x = self.norm(x)x = self.pwconv1(x)x = self.act(x)x = self.grn(x)x = self.pwconv2(x)x = x.permute(0, 3, 1, 2)x = input + self.drop_path(x)return xclass ConvNeXtV2(nn.Module):def __init__(self, in_chans=3, num_classes=1000, depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], drop_path_rate=0., head_init_scale=1.):super().__init__()self.depths = depthsself.downsample_layers = nn.ModuleList()stem = nn.Sequential(nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),LayerNorm(dims[0], eps=1e-6, data_format="channels_first"))self.downsample_layers.append(stem)for i in range(3):downsample_layer = nn.Sequential(LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),nn.Conv2d(dims[i], dims[i+1], kernel_size=2, stride=2),)self.downsample_layers.append(downsample_layer)self.stages = nn.ModuleList()dp_rates=[x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] cur = 0for i in range(4):stage = nn.Sequential(*[Block(dim=dims[i], drop_path=dp_rates[cur + j]) for j in range(depths[i])])self.stages.append(stage)cur += depths[i]self.norm = nn.LayerNorm(dims[-1], eps=1e-6)self.head = nn.Linear(dims[-1], num_classes)self.apply(self._init_weights)self.channel = [i.size(1) for i in self.forward(torch.randn(1, 3, 640, 640))]def _init_weights(self, m):if isinstance(m, (nn.Conv2d, nn.Linear)):trunc_normal_(m.weight, std=.02)nn.init.constant_(m.bias, 0)def forward(self, x):res = []for i in range(4):x = self.downsample_layers[i](x)x = self.stages[i](x)res.append(x)return res

该程序文件是一个实现了ConvNeXt V2模型的PyTorch代码。ConvNeXt V2是一个用于图像分类任务的卷积神经网络模型。该文件定义了ConvNeXtV2类和一些辅助类和函数。

ConvNeXtV2类是整个模型的主要部分。它包含了多个Block模块和一些卷积层、归一化层和线性层。每个Block模块由深度可分离卷积、归一化、线性层、激活函数和GRN层组成。模型的输入经过一系列的下采样层和Block模块后,最后经过归一化层和线性层得到分类结果。

除了ConvNeXtV2类,该文件还定义了一些辅助类和函数,如LayerNorm类用于实现不同数据格式的归一化,GRN类用于实现全局响应归一化,Block类用于实现ConvNeXtV2的基本模块,以及一些用于加载预训练权重的函数。

最后,该文件还定义了一些不同规模的ConvNeXtV2模型的函数,如convnextv2_atto、convnextv2_femto等,这些函数可以根据输入的权重文件路径和其他参数返回对应规模的ConvNeXtV2模型。

总之,该程序文件实现了ConvNeXt V2模型,并提供了加载预训练权重和创建不同规模模型的功能。

5.6 backbone\CSwomTramsformer.py
class CSWinTransformer(nn.Module):def __init__(self, img_size=224, patch_size=4, in_chans=3, num_classes=1000, embed_dim=96, depths=[2, 2, 6, 2], num_heads=[3, 6, 12, 24], mlp_ratio=4., qkv_bias=True, qk_scale=None, drop_rate=0., attn_drop_rate=0., drop_path_rate=0., norm_layer=nn.LayerNorm):super().__init__()self.num_classes = num_classesself.depths = depthsself.num_features = self.embed_dim = embed_dimself.patch_embed = PatchEmbed(img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim)self.pos_drop = nn.Dropout(p=drop_rate)dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]  # stochastic depth decay ruleself.blocks = nn.ModuleList([CSWinBlock(dim=embed_dim, reso=img_size // patch_size, num_heads=num_heads[i], mlp_ratio=mlp_ratio,qkv_bias=qkv_bias, qk_scale=qk_scale, drop=drop_rate, attn_drop=attn_drop_rate,drop_path=dpr[sum(depths[:i]):sum(depths[:i + 1])], norm_layer=norm_layer,last_stage=(i == len(depths) - 1))for i in range(len(depths))])self.norm = norm_layer(embed_dim)self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity()trunc_normal_(self.head.weight, std=0.02)zeros_(self.head.bias)def init_weights(self, pretrained=None):if isinstance(pretrained, str):load_pretrained(self, pretrained, num_classes=self.num_classes)elif pretrained is None:self.apply(self._init_weights)else:raise TypeError('pretrained must be a str or None')def _init_weights(self, m):if isinstance(m, nn.Linear):trunc_normal_(m.weight, std=.02)if isinstance(m, nn.Linear) and m.bias is not None:zeros_(m.bias)elif isinstance(m, nn.LayerNorm):ones_(m.weight)zeros_(m.bias)elif isinstance(m, nn.Conv2d):trunc_normal_(m.weight, std=.02)if isinstance(m, nn.Conv2d) and m.bias is not None:zeros_(m.bias)def forward_features(self, x):x = self.patch_embed(x)x = self.pos_drop(x)for blk in self.blocks:x = blk(x)x = self.norm(x)return xdef forward(self, x):x = self.forward_features(x)x = x.mean(dim=1)  # global average poolingif self.num_classes > 0:x = self.head(x)return x

该程序文件是一个用于图像分类的模型CSWin Transformer的实现。CSWin Transformer是一种基于ViT的图像分类模型,通过将图像划分为多个窗口,并使用LePEAttention模块对窗口进行注意力计算,从而捕捉图像的全局信息。模型包括CSWinBlock模块和Merge_Block模块,其中CSWinBlock模块用于处理每个窗口的特征,Merge_Block模块用于将窗口特征合并为整个图像的特征。模型还包括Mlp模块和LePEAttention模块,用于特征的全连接和注意力计算。

6.系统整体结构

下面是对每个文件的功能进行整理的Markdown表格:

文件路径功能
export.py导出模型到不同的格式
predict.py运行模型进行预测
train.py训练模型
ui.py创建图形用户界面
backbone\convnextv2.py实现ConvNeXt V2模型
backbone\CSwomTramsformer.py实现CSWinTransformer模型
backbone\EfficientFormerV2.py实现EfficientFormer V2模型
backbone\efficientViT.py实现EfficientViT模型
backbone\fasternet.py实现Fasternet模型
backbone\lsknet.py实现LSKNet模型
backbone\repvit.py实现RepVIT模型
backbone\revcol.py实现RevCoL模型
backbone\SwinTransformer.py实现Swin Transformer模型
backbone\VanillaNet.py实现VanillaNet模型
extra_modules\afpn.py实现AFPN模块
extra_modules\attention.py实现注意力模块
extra_modules\block.py实现基本模块
extra_modules\dynamic_snake_conv.py实现动态蛇卷积模块
extra_modules\head.py实现模型的头部部分
extra_modules\kernel_warehouse.py存储卷积核的仓库
extra_modules\orepa.py实现OREPA模块
extra_modules\rep_block.py实现REP模块
extra_modules\RFAConv.py实现RFAConv模块
extra_modules_init_.py初始化extra_modules模块
extra_modules\ops_dcnv3\setup.py设置DCNv3模块
extra_modules\ops_dcnv3\test.py测试DCNv3模块
extra_modules\ops_dcnv3\functions\dcnv3_func.py实现DCNv3模块的函数
extra_modules\ops_dcnv3\functions_init_.py初始化DCNv3模块的函数
extra_modules\ops_dcnv3\modules\dcnv3.py实现DCNv3模块
extra_modules\ops_dcnv3\modules_init_.py初始化DCNv3模块
models\common.py包含一些通用的模型函数
models\experimental.py包含一些实验性的模型
models\tf.py包含一些TensorFlow模型
models\yolo.py包含YOLO模型
models_init_.py初始化models模块
segment\predict.py运行分割模型进行预测
segment\train.py训练分割模型
segment\val.py验证分割模型
ultralytics_init_.py初始化ultralytics模块
ultralytics\cfg_init_.py初始化cfg模块
ultralytics\data\annotator.py数据标注工具
ultralytics\data\augment.py数据增强工具
ultralytics\data\base.py数据集基类

7.YOLOv8模型原理

YOLOv8是YOLO系列最新的模型,具有非常优秀的检测精度和速度。根据网络的深度与特征图的宽度大小, YOLOv8算法分为:YOLOv8-n、YOLOv8一s 、YOLOv8-m 、 YOLOv8-l、和 YOLOv8-x 5个版本。按照网络结构图,YOLOv8可分为: Inpul 、 Backbone , Neck和Head 4部分。
在这里插入图片描述

Backbone采用了CSPDarknet 架构,由CBS (标准卷积层)、C2f模块和 SPPF(金字塔池化)组成。通过5次标准卷积层和C2f模块逐步提取图像特征,并在网络末尾添加SPPF模块,将任意大小的输入图像转换成固定大小的特征向量。分别取P3、P4、P5层的特征提取结果,向Head输出80×80、40 × 40、20×20三个尺度的特征层。
C2f模块借鉴了残差网络(ResNet)以及ELAN的思想,其结构分为两个分支,主干部分利用Bottleneckm2%模块逐步加深网络,分支部分保留输入层通道并与主干部分特征进行融合,如图所示。通过标准卷积层提取新的特征层,相比于YOLOv5使用的C3模块,C2f模块可以在卷积层和全连接层之间建立一个平滑的转换,从而实现了参数的共享,提高了模型的效率和泛化能力。
Head采用了PAN-FPN 结构,将 Backbone输入的3个特征层进行多尺度融合,进行自顶向下(FAN)和自底向上 (PAN)的特征传递,对金字塔进行增强,使不同尺寸的特征图都包含强目标语义信息和强目标特征信息,保证了对不同尺寸样本的准确预测。
Detect借鉴了Decoupled-Head 思想,用一个解耦检测头将输入的不同尺寸特征层分成2个分支进行检测。第1个分支在进行3次卷积后使进行回归任务,输出预测框。第2个分支在进行3次卷积后进行分类任务,输出类别的概率。采用Varifocal_Loss2”作为损失函数,其式为:

在这里插入图片描述

8.Context_Grided_Network(CGNet)简介

参考该博客提出的一种轻量化语义分割模型Context Grided Network(CGNet),以满足设备的运行需要。

CGNet主要由CG块构建而成,CG块可以学习局部特征和周围环境上下文的联合特征,最后通过引入全局上下文特征进一步改善联合特征的学习。
在这里插入图片描述

下图给出了在Cityscapes数据集上对现有的一些语义分割模型的测试效果,横轴表示参数量,纵轴表示准确率(mIoU)。可以看出,在参数量较少的情况下,CGNet可以达到一个比较好的准确率。虽与高精度模型相去甚远,但在一些对精度要求不高、对实时性要求比较苛刻的情况下,很有价值。

高精度模型,如DeepLab、DFN、DenseASPP等,动不动就是几十M的参数,很难应用在移动设备上。而上图中红色的模型,相对内存占用较小,但它们的分割精度却不是很高。作者认为主要原因是,这些小网络大多遵循着分类网络的设计思路,并没有考虑语义分割任务更深层次的特点。

空间依赖性和上下文信息对提高分割精度有很大的作用。作者从该角度出发,提出了CG block,并进一步搭建了轻量级语义分割网络CGNet。CG块具有以下特点:

学习局部特征和上下文特征的联合特征;
通过全局上下文特征改进上述联合特征;
可以贯穿应用在整个网络中,从low level(空间级别)到high level(语义级别)。不像PSPNet、DFN、DenseASPP等,只在编码阶段以后捕捉上下文特征。;
只有3个下采样,相比一般5个下采样的网络,能够更好地保留边缘信息。
CGNet遵循“深而薄”的原则设计,整个网络又51层构成。其中,为了降低计算,大量使用了channel-wise conv.

小型语义分割模型:

需要平衡准确率和系统开销
进化路线:ENet -> ICNet -> ESPNet
这些模型基本都基于分类网络设计,在分割准确率上效果并不是很好
上下文信息模型:

大多数现有模型只考虑解码阶段的上下文信息并且没有利用周围的上下文信息
注意力机制:

CG block使用全局上下文信息计算权重向量,并使用其细化局部特征和周围上下文特征的联合特征

Context Guided Block

CG block由4部分组成:
在这里插入图片描述

此外,CG block还采用了残差学习。文中提出了局部残差学习(LRL)和全局残差学习(GRL)两种方式。 LRL添加了从输入到联合特征提取器的连接,GRL添加了从输入到全局特征提取器的连接。从直观上来说,GRL比LRL更能促进网络中的信息传递(更像ResNet~~),后面实验部分也进行了测试,的确GRL更能提升分割精度。

在这里插入图片描述

CGNet的通用网络结构如下图所示,分为3个stage,第一个stage使用3个卷积层抽取特征,第二和第三个stage堆叠一定数量的CG block,具体个数可以根据情况调整。最后,通过1x1 conv得到分割结果。

在这里插入图片描述

下图是用于Cityscapes数据集的CGNet网络细节说明:输入尺寸为3680680;stage1连续使用了3个Conv-BN-PReLU组合,首个组合使用了stride=2的卷积,所以得到了1/2分辨率的feature map;stage2和stage3分别使用了多个CG block,且其中使用了不同大小的膨胀卷积核,最终分别得到了1/4和1/8的feature map。

需注意:

stage2&3的输入特征分别由其上一个stage的首个和最后一个block组合给出(参考上图的绿色箭头);

输入注入机制,图中未体现,实际使用中,作者还将输入图像下采样1/4或1/8,分别给到stage2和stage3的输入中 ,以进一步加强特征传递。

channel-wise conv。为了缩减参数数量,在局部特征提取器和周围上下文特征提取器中使用了channel-wise卷积,可以消除跨通道的计算成本,同时节省内存占用。但是,没有像MobileNet等模型一样,在depth-wise卷积后面接point-wise卷积(11 conv),作者解释是,因为CG block需要保持局部特征和周围上下文特征的独立性,而11 conv会破坏这种独立性,所以效果欠佳,实验部分也进行了验证。

个人感觉此处应该指的是depth-wise卷积?

官方Git中对该部分的实现如下:

在这里插入图片描述

9.训练结果可视化分析

评价指标

Epoch:训练迭代次数。
Train Losses:训练过程中的损失指标,包括框损失、分割损失、对象损失和类损失。
精度和召回率指标:对于背景 (B) 和主要对象 (M),包括不同 IOU(并集交集)阈值下的 mAP(平均平均精度)。
验证损失:与训练中类似的损失指标,但针对验证数据集。
Learning Rates (x/lr0, x/lr1, x/lr2):训练过程中不同的学习率值。
为了进行详细分析,我们可以从不同的角度可视化和解释这些指标。这将包括:

训练结果可视化

对各个时期的训练和验证损失进行趋势分析,以了解模型的学习进度。
检查精确度和召回率指标,以评估模型的准确性及其检测数据中相关特征的能力。
观察学习率变化及其对模型性能的影响。
让我们首先通过各种图表来可视化这些方面。

# Attempting a different approach for plotting
# Extracting the epoch values for the x-axis
epochs = data['epoch']# Plotting the training and validation losses
plt.figure(figsize=(15, 10))# Box Loss
plt.subplot(2, 2, 1)
plt.plot(epochs, data['train/box_loss'], label='Train Box Loss')
plt.plot(epochs, data['val/box_loss'], label='Val Box Loss')
plt.title('Box Loss')
plt.legend()# Segmentation Loss
plt.subplot(2, 2, 2)
plt.plot(epochs, data['train/seg_loss'], label='Train Segmentation Loss')
plt.plot(epochs, data['val/seg_loss'], label='Val Segmentation Loss')
plt.title('Segmentation Loss')
plt.legend()# Object Loss
plt.subplot(2, 2, 3)
plt.plot(epochs, data['train/obj_loss'], label='Train Object Loss')
plt.plot(epochs, data['val/obj_loss'], label='Val Object Loss')
plt.title('Object Loss')
plt.legend()# Class Loss
plt.subplot(2, 2, 4)
plt.plot(epochs, data['train/cls_loss'], label='Train Class Loss')
plt.plot(epochs, data['val/cls_loss'], label='Val Class Loss')
plt.title('Class Loss')
plt.legend()plt.tight_layout()
plt.show()

在这里插入图片描述
可视化现在可以正确显示各个时期的训练和验证损失:

框丢失:显示模型预测对象周围边界框的效果。下降趋势表明识别对象边界的性能有所提高。

分割损失:表示模型分割图像的能力。值越低表明分割精度越高。

对象丢失:反映模型在边界框中对象检测的性能。

类别损失:与模型对检测到的对象进行分类的准确性有关。

从这些图中,我们可以观察每种类型的损失在训练和验证过程中如何演变,从而深入了解模型的学习有效性和泛化能力。

10.系统整合

下图完整源码&数据集&环境部署视频教程&自定义UI界面

在这里插入图片描述

参考博客《【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统》

11.参考文献


[1]李伟华,刘彬,付遨,等.扫描间距对选区激光熔化成形CoCrFeNiMo0.2高熵合金微观结构及性能的影响[J].中南大学学报(自然科学版).2023,54(1).DOI:10.11817/j.issn.1672-7207.2023.01.003 .

[2]陈泽坤,李晓雁.金属增材制造过程中材料微观组织演化的模拟研究[J].力学进展.2022,52(2).DOI:10.6052/1000-0992-22-021 .

[3]廉艳平,王潘丁,高杰,等.金属增材制造若干关键力学问题研究进展[J].力学进展.2021,(3).DOI:10.6052/1000-0992-21-037 .

[4]李淮阳,黎振华,杨睿,等.选区激光熔化金属表面成形质量控制的研究进展[J].表面技术.2020,(9).DOI:10.16490/j.cnki.issn.1001-3660.2020.09.012 .

[5]朱家明.基于图像分析软件的晶粒尺寸分布统计[J].压电与声光.2013,(4).DOI:10.3969/j.issn.1004-2474.2013.04.030 .

[6]顾海,张捷,孙健华,等.激光熔化沉积2195铝锂合金微观组织演变及力学性能[J].金属热处理.2023,48(1).DOI:10.13251/j.issn.0254-6051.2023.01.010 .

[7]门正兴.热处理对激光选区熔化18Ni300不锈钢组织及力学性能的影响[J].应用激光.2022,42(11).DOI:10.14128/j.cnki.al.20224211.029 .

[8]李继康,张振武,杨源褀,等.激光选区熔化DD91镍基单晶高温合金的单道形貌、晶体取向和微观组织[J].中国激光.2022,49(14).DOI:10.3788/CJL202249.1402103 .

[9]蒋华臻,房佳汇钰,陈启生,等.激光选区熔化成形316L不锈钢工艺、微观组织、力学性能的研究现状[J].中国激光.2022,49(14).DOI:10.3788/CJL202249.1402804 .

[10]敖晓辉,刘检华,夏焕雄,等.选择性激光熔化工艺的介-微观建模与仿真方法综述[J].机械工程学报.2022,58(5).DOI:10.3901/JME.2022.05.239 .

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

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

相关文章

Makefile从零基础到入门到熟练使用

Makefile从零基础到入门到熟练使用 一、了解Makefile作用 1、一个文件从源码到可执行文件的过程&#xff1a;预处理-编译-汇编-链接 2、每个步骤都可以单独执行 3、使用过程中会压缩成两步 4、一步执行并且打印详细日志信息 gcc -o hello hello.c -v5、有多个源码文件需要…

【小白专用】MySQL入门(详细总结)

3. 创建数据库 使用 create database 数据库名; 创建数据库。 create database MyDB_one; create database DBAliTest; 创建数据库成功后&#xff0c;数据库的数量变成了6个&#xff0c;多了刚才创建的 dbalitest 。 4. 创建数据库时设置字符编码 使用 create database 数据…

【文件上传系列】No.1 大文件分片、进度图展示(原生前端 + Node 后端 Koa)

分片&#xff08;500MB&#xff09;进度效果展示 效果展示&#xff0c;一个分片是 500MB 的 分片&#xff08;10MB&#xff09;进度效果展示 大文件分片上传效果展示 前端 思路 前端的思路&#xff1a;将大文件切分成多个小文件&#xff0c;然后并发给后端。 页面构建 先在页…

【算法题】拼接URL(js)

自己的解法&#xff1a; 2x2 种情况判断 function solution(urlStr) {const pre urlStr.split(",")[0];const after urlStr.split(",")[1];if (pre.endsWith("/")) {if (after.startsWith("/")) {return pre after.slice(1);} else…

如何跑AI模型—ultralytics

这里以跑 ultralytics 为示例&#xff0c;记录了如何从 0-1 跑个简单的模型&#xff0c;包括环境搭建。我的是 Window 系统&#xff0c;其他系统也类似。 主要流程是环境搭建&#xff0c;找个官网的 demo&#xff0c;收集好所需素材&#xff08;模型&#xff0c;图片等&#x…

“四位一体”引领企业数据治理新模式

数字化时代&#xff0c;数据作为新的生产要素受到了前所未有的关注和重视。 随着企业业务的发展&#xff0c;数据积累越来越多。怎么管理好数据&#xff1f;如何利用好数据&#xff1f;数据价值如何挖掘&#xff1f;成为很多企业面临的难题&#xff01; 面对这些棘手的问题&am…

做好这三点,有效提升项目的成功率

近期&#xff0c;大家都在做年度工作总结的准备了&#xff0c;在互相交流的时候&#xff0c;提到了一个值得思考的话题——“如何能提高项目的成功率&#xff1f;”。对于项目经理来说&#xff0c;都希望能找到一些通用的经验直接复制粘贴在自己的项目上&#xff0c;促进项目的…

鸿蒙原生应用开发【分布式数据对象】

01、什么是分布式数据对象 在可信组网环境下&#xff0c;多个相互组网认证的设备将各自创建的对象加入同一个 sessionId&#xff0c;使得加入的多个数据对象之间可以同步数据&#xff0c;也就是说&#xff0c;当某一数据对象属性发生变更时&#xff0c;其他数据对象会检测到这…

优秀软件测试工程师必备的“8个能力”

首先要说&#xff0c;做软件测试不难&#xff0c;难的是做好软件测试。 结合自己这些年的工作经验&#xff0c;自己也总结出来8个方面的能力&#xff0c;可能有些方面感觉要求暂时还达不到&#xff0c;但这些确实是做软件测试工作所必备的能力&#xff0c;掌握了这8个方面的能力…

Mybatis XML改查操作(结合上文)

"改"操作 先在UserInfoXMLMapper.xml 中 : <?xml version"1.0" encoding"UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><map…

vue使用甘特图dhtmlxgantt + gantt.addTaskLayer

效果图&#xff1a; 甘特图 官网地址 gantt安装与使用 vue版---部分功能收费 安装gantt 或 引入文件 npm install dhtmlx-gantt -save或import gantt from "/public/static/dhtmlxgantt/dhtmlxgantt.js"; import "/public/static/dhtmlxgantt/locale/local…

【银行测试】金融项目+测试方法范围分析,功能/接口/性能/安全...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、金融行业软件特…