【改进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。

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第1张图片

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 json

import cv2
import pandas as pd
from PIL import Image
from collections import defaultdict

from utils import *


# Convert INFOLKS JSON file into YOLO-format labels ----------------------------
def convert_infolks_json(name, files, img_path):
    # Create folders
    path = make_dirs()

    # Import json
    data = []
    for file in glob.glob(files):
        with open(file) as f:
            jdata = json.load(f)
            jdata['json_file'] = file
            data.append(jdata)

    # Write images and shapes
    name = path + os.sep + name
    file_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

        # filename
        with open(name + '.txt', 'a') as file:
            file.write('%s\n' % f)

    # Write *.names file
    names = sorted(np.unique(cat))
    # names.pop(names.index('Missing product'))  # remove
    with open(name + '.names', 'a') as file:
        [file.write('%s\n' % a) for a in names]

    # Write labels file
    for 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  # skip

                category_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 width
                box[[1, 3]] /= wh[i][1]  # normalize y by height
                box = [box[[0, 2]].mean(), box[[1, 3]].mean(), box[2] - box[0], box[3] - box[1]]  # xywh
                if (box[2] > 0.) and (box[3] > 0.):  # if w > 0 and h > 0
                    file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))

    # Split data into train, test, and validate files
    split_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 folders
    path = make_dirs()
    name = path + os.sep + name

    # Import json
    data = []
    for file in glob.glob(files):
        with open(file) as f:
            jdata = json.load(f)
            jdata['json_file'] = file
            data.append(jdata)

    # Get all categories
    file_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 file
    names = sorted(pd.unique(cat))
    with open(name + '.names', 'a') as file:
        [file.write('%s\n' % a) for a in names]

    # Write labels file
    n1, n2 = 0, 0
    missing_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 += 1
            if (len(f) > 0) and (wh[0] > 0) and (wh[1] > 0):
                n2 += 1

                # append filename to list
                with open(name + '.txt', 'a') as file:
                    file.write('%s\n' % f)

                # write labelsfile
                label_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 width
                        box[[1, 3]] /= wh[1]  # normalize y by height
                        box = [box[0] + box[2] / 2, box[1] + box[3] / 2, box[2], box[3]]  # xywh

                        if (box[2] > 0.) and (box[3] > 0.):  # if w > 0 and h > 0
                            file.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 files
    split_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 folders
    dir = make_dirs()  # output directory

    jsons = []
    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 json
    n1, n2, n3 = 0, 0, 0
    missing_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 file
        for 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 file
            if len(f):
                f = f[0]
                file_name.append(f)
                wh = exif_size(Image.open(f))  # (width, height)

                n1 += 1  # all images
                if len(f) > 0 and wh[0] > 0 and wh[1] > 0:
                    label_file = dir + 'labels/' + Path(f).stem + '.txt'

                    nlabels = 0
                    try:
                        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-class

                            for 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 width
                                box[[1, 3]] /= wh[1]  # normalize y by height
                                box = [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 > 0
                                    file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))
                                    n3 += 1
                                    nlabels += 1

                        if nlabels == 0:  # remove non-labelled images from dataset
                            os.system(f'rm {label_file}')
                            # print('no labels for %s' % f)
                            continue  # next file

                        # write image
                        img_size = 4096  # resize to maximum
                        img = cv2.imread(f)  # BGR
                        assert img is not None, 'Image Not Found ' + f
                        r = img_size / max(img.shape)  # size ratio
                        if r < 1:  # downsize if necessary
                            h, w, _ = img.shape
                            img = cv2.resize(img, (int(w * r), int(h * r)), interpolation=cv2.INTER_AREA)

                        ifile = dir + 'images/' + Path(f).name
                        if cv2.imwrite(ifile, img):  # if success append image to list
                            with open(dir + 'data.txt', 'a') as file:
                                file.write('%s\n' % ifile)
                            n2 += 1  # correct images

                    except Exception:
                        os.system(f'rm {label_file}')
                        print(f'problem with {f}')

            else:
                missing_images.append(image_file)

    nm = len(missing_images)  # number missing
    print('\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 file
    names = ['knife']  # preserves sort order
    with open(dir + 'data.names', 'w') as f:
        [f.write('%s\n' % a) for a in names]

    # Split data into train, test, and validate files
    split_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 directory
    coco80 = coco91_to_coco80_class()

    # Import json
    for json_file in sorted(Path(json_dir).resolve().glob('*.json')):
        fn = Path(save_dir) / 'labels' / json_file.stem.replace('instances_', '')  # folder name
        fn.mkdir()
        with open(json_file) as f:
            data = json.load(f)

        # Create image dict
        images = {'%g' % x['id']: x for x in data['images']}
        # Create image-annotations dict
        imgToAnns = defaultdict(list)
        for ann in data['annotations']:
            imgToAnns[ann['image_id']].append(ann)

        # Write labels file
        for 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 center
                box[[0, 2]] /= w  # normalize x
                box[[1, 3]] /= h  # normalize y
                if box[2] <= 0 or box[3] <= 0:  # if w <= 0 and h <= 0
                    continue

                cls = coco80[ann['category_id'] - 1] if cls91to80 else ann['category_id'] - 1  # class
                box = [cls] + box.tolist()
                if box not in bboxes:
                    bboxes.append(box)
                # Segments
                if 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 concatenated
                        s = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()
                    s = [cls] + s
                    if s not in segments:
                        segments.append(s)

            # Write
            with 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 segments
                    file.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 segment
    for 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 segments
    for k in range(2):
        # forward connection
        if k == 0:
            for i, idx in enumerate(idx_list):
                # middle segments have two indexes
                # reverse the index of middle segments
                if 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 one
                if 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 s


def delete_dsstore(path='../datasets'):
    # Delete apple .DS_store files
    from pathlib import Path
    files = 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 *.json
                          use_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/VoTT
        convert_vott_json(name='data',
                          files='../../Downloads/athena_day/20190715/*.json',
                          img_path='../../Downloads/athena_day/20190715/')  # images folder

    elif source == 'ath':  # ath format
        convert_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_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

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_first

class 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 = False
        workers = 0
        return build_dataloader(dataset, batch_size, workers, shuffle, rank)

    def preprocess_batch(self, batch):
        batch['img'] = batch['img'].to(self.device, non_blocking=True).float() / 255
        return batch

    def set_model_attributes(self):
        self.model.nc = self.data['nc']
        self.model.names = self.data['names']
        self.model.args = self.args

    def 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 model

    def 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 keys

    def 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 = eps
        self.data_format = data_format
        if 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 x

class 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 + x

class 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 = x
        x = 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 x

class 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 = depths
        self.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 = 0
        for 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_classes
        self.depths = depths
        self.num_features = self.embed_dim = embed_dim

        self.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 rule
        self.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 x

    def forward(self, x):
        x = self.forward_features(x)
        x = x.mean(dim=1)  # global average pooling
        if 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部分。
【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第2张图片

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块可以学习局部特征和周围环境上下文的联合特征,最后通过引入全局上下文特征进一步改善联合特征的学习。
【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第3张图片

下图给出了在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部分组成:
【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第4张图片

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

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第5张图片

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

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第6张图片

下图是用于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中对该部分的实现如下:

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第7张图片

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()

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第8张图片
可视化现在可以正确显示各个时期的训练和验证损失:

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

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

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

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

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

10.系统整合

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

【改进YOLOv8】融合Context_Grided_Network(CGNet)的晶粒大小分布统计系统_第9张图片

参考博客《【改进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 .

你可能感兴趣的:(YOLO,目标跟踪,人工智能)