【精选实战】基于OpenCV和深度学习的建筑语义分割和色彩聚类分析系统

2023-12-28 15:38:42

1.研究背景与意义

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

研究背景与意义

随着城市化进程的加速和建筑行业的快速发展,对建筑物的分析和理解变得越来越重要。建筑语义分割和色彩聚类分析系统是一种基于计算机视觉和深度学习技术的创新方法,可以对建筑物进行自动化的分析和识别。这一系统的研究和应用将为建筑行业带来许多潜在的好处。

首先,建筑语义分割和色彩聚类分析系统可以提供对建筑物的详细分析。通过对建筑物进行语义分割,系统可以识别出建筑物的不同部分,如墙壁、窗户、门等。这将有助于建筑师和设计师更好地理解建筑物的结构和功能,从而提供更好的设计和规划建议。此外,系统还可以通过色彩聚类分析建筑物的颜色分布,为建筑师提供更多的灵感和创意。

其次,建筑语义分割和色彩聚类分析系统可以用于建筑物的智能监控和安全管理。通过对建筑物进行实时的语义分割和色彩聚类分析,系统可以检测出异常情况,如火灾、入侵等。这将有助于提高建筑物的安全性和保护人员的生命财产安全。

此外,建筑语义分割和色彩聚类分析系统还可以用于建筑物的维护和管理。通过对建筑物进行语义分割,系统可以识别出建筑物的不同部分,如墙壁、屋顶、地板等。这将有助于建筑师和维护人员更好地了解建筑物的结构和状况,从而提供更好的维护和管理建议。此外,系统还可以通过色彩聚类分析建筑物的颜色分布,为维护人员提供更多的信息和指导。

最后,建筑语义分割和色彩聚类分析系统还可以用于建筑物的文化遗产保护和旅游推广。通过对建筑物进行语义分割和色彩聚类分析,系统可以提取出建筑物的特征和风格,从而为文化遗产保护和旅游推广提供更多的信息和资源。这将有助于提高建筑物的知名度和吸引力,促进旅游业的发展。

综上所述,基于OpenCV和深度学习的建筑语义分割和色彩聚类分析系统具有广泛的应用前景和重要的研究意义。通过对建筑物进行自动化的分析和识别,该系统可以提供对建筑物的详细分析、智能监控和安全管理、维护和管理以及文化遗产保护和旅游推广等方面的支持。这将为建筑行业带来许多潜在的好处,并推动建筑行业的创新和发展。

2.图片演示

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

3.视频演示

基于OpenCV和深度学习的建筑语义分割和色彩聚类分析系统_哔哩哔哩_bilibili

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

图片的收集

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

在这里插入图片描述

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     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.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     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.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     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


def export_formats():
    # YOLOv5 export formats
    x = [
        ['PyTorch', '-', '.pt', True, True],
        ['TorchScript', 'torchscript', '.torchscript', True, True],
        ['ONNX', 'onnx', '.onnx', True, True],
        ['OpenVINO', 'openvino', '_openvino_model', True, False],
        ['TensorRT', 'engine', '.engine', False, True],
        ['CoreML', 'coreml', '.mlmodel', True, False],
        ['TensorFlow SavedModel', 'saved_model', '_saved_model', True, True],
        ['TensorFlow GraphDef', 'pb', '.pb', True, True],
        ['TensorFlow Lite', 'tflite', '.tflite', True, False],
        ['TensorFlow Edge TPU', 'edgetpu', '_edgetpu.tflite', False, False],
        ['TensorFlow.js', 'tfjs', '_web_model', False, False],
        ['PaddlePaddle', 'paddle', '_paddle_model', True, True],]
    return pd.DataFrame(x, columns=['Format', 'Argument', 'Suffix', 'CPU', 'GPU'])


def try_export(inner_func):
    # YOLOv5 export decorator, i..e @try_export
    inner_args = get_default_args(inner_func)

    def outer_func(*args, **kwargs):
        prefix = inner_args['prefix']
        try:
            with Profile() as dt:
                f, model = inner_func(*args, **kwargs)
            LOGGER.info(f'{prefix} export success ? {dt.t:.1f}s, saved as {f} ({file_size(f):.1f} MB)')
            return f, model
        except Exception as e:
            LOGGER.info(f'{prefix} export failure ? {dt.t:.1f}s: {e}')
            return None, None

    return outer_func


@try_export
def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')):
    # YOLOv5 TorchScript model export
    LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...')
    f = file.with_suffix('.torchscript')

    ts = torch.jit.trace(model, im, strict=False)
    d = {"shape": im.shape, "stride": int(max(model.stride)), "names": model.names}
    extra_files = {'config.txt': json.dumps(d)}  # torch._C.ExtraFilesMap()
    if optimize:  # https://pytorch.org/tutorials/recipes/mobile_interpreter.html
        optimize_for_mobile(ts)._save_for_lite_interpreter(str(f), _extra_files=extra_files)
    else:
        ts.save(str(f), _extra_files=extra_files)
    return f, None


@try_export
def export_onnx(model, im, file, opset, dynamic, simplify, prefix=colorstr('ONNX:')):
    # YOLOv5 ONNX export
    check_requirements('onnx>=1.12.0')
    import onnx

    LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...')
    f = file.with_suffix('.onnx')

    output_names = ['output0', 'output1'] if isinstance(model, SegmentationModel) else ['output0']
    if dynamic:
        dynamic = {'images': {0: 'batch', 2: 'height', 3: 'width'}}  # shape(1,3,640,640)
        if isinstance(model, SegmentationModel):
            dynamic['output0'] = {0: 'batch', 1: 'anchors'}  # shape(1,25200,85)
            dynamic['output1'] = {0: 'batch', 2: 'mask_height', 3: 'mask_width'}  # shape(1,32,160,160)
        elif isinstance(model, DetectionModel):
            dynamic['output0'] = {0: 'batch', 1: 'anchors'}  # shape(1,25200,85)

    torch.onnx.export(
        model.cpu() if dynamic else model,  # --dynamic only compatible with cpu
        im.cpu() if dynamic else im,
        f,
        verbose=False,
        opset_version=opset,
        do_constant_folding=True,  # WARNING: DNN inference with torch>=1.12 may require do_constant_folding=False
        input_names=['images'],
        output_names=output_names,
        dynamic_axes=dynamic or None)

    # Checks
    model_onnx = onnx.load(f)  # load onnx model
    onnx.checker.check_model(model_onnx)  # check onnx model

    # Metadata
    d = {'stride': int(max(model.stride)), 'names': model.names}
    for k, v in d.items():
        meta = model_onnx.metadata_props.add()
        meta.key, meta.value = k, str(v)
    onnx.save(model_onnx, f)

    # Simplify
    if simplify:
        try:
            cuda = torch.cuda.is_available()
            check_requirements(('onnxruntime-gpu' if cuda else 'onnxruntime', 'onnx-simplifier>=0.4.1'))
            import onnxsim

            LOGGER.info(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...')
            model_simp, check = onnxsim.simplify(f, check=True)
            assert check, 'assert check failed'
            onnx.save(model_simp, f)
        except Exception as e:
            LOGGER.info(f'{prefix} simplifier failure {e}')
    return f, None

export.py是一个用于将YOLOv5 PyTorch模型导出为其他格式的程序文件。该文件提供了多种导出格式,包括TorchScript、ONNX、OpenVINO、TensorRT、CoreML、TensorFlow SavedModel、TensorFlow GraphDef、TensorFlow Lite、TensorFlow Edge TPU、TensorFlow.js和PaddlePaddle。使用该文件可以将YOLOv5模型导出为所需的格式,以便在不同的平台和框架上进行推理。

该文件还提供了一些辅助函数和装饰器,用于处理模型导出过程中的异常情况和输出日志。导出过程中会根据导出格式的不同,调用相应的导出函数进行模型导出。导出过程中还会进行一些检查和优化操作,以确保导出的模型的正确性和性能。

使用该文件时,可以通过命令行参数指定要导出的模型文件和导出格式。导出过程中会输出导出成功或失败的日志信息,并将导出的模型保存到指定的文件中。

除了导出功能,该文件还提供了一些其他功能,如模型加载、图像加载和大小检查等。这些功能可以在导出过程中进行一些前置操作和检查,以确保导出的模型和输入数据的正确性。

总之,export.py是一个用于将YOLOv5 PyTorch模型导出为其他格式的程序文件,提供了多种导出格式和一些辅助功能,可以方便地在不同的平台和框架上进行模型推理。

5.2 process.py

这个程序文件名为process.py,主要功能是将images文件夹中的图片进行处理,并保存到test文件夹中。具体步骤如下:

  1. 导入必要的模块:os模块用于操作文件和文件夹,PIL模块用于处理图片。
  2. 检查并创建test文件夹:如果test文件夹不存在,则创建该文件夹。
  3. 读取images文件夹中的图片并进行处理:遍历images文件夹中的所有文件名,如果文件名以jpg、png、jpeg、tif或bmp结尾,则进行处理。
  4. 打开图片并只保留右半部分:使用Image.open()函数打开图片,然后使用crop()函数截取图片的右半部分。
  5. 将右半部分resize为400x400:使用resize()函数将截取的右半部分图片调整为400x400的大小。
  6. 保存处理后的图片到test文件夹中:使用save()函数将处理后的图片保存到test文件夹中,保存的文件名与原始文件名相同。
  7. 输出处理完成的提示信息。

总体来说,这个程序的功能是将images文件夹中的图片进行处理,只保留右半部分并调整大小后保存到test文件夹中。

5.2 ui.py


class YOLOv5Detector:
    def __init__(self):
        FILE = Path(__file__).resolve()
        ROOT = FILE.parents[1]  # YOLOv5 root directory
        if str(ROOT) not in sys.path:
            sys.path.append(str(ROOT))  # add ROOT to PATH
        ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative

        from models.common import DetectMultiBackend
        from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
        from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2,
                                   increment_path, non_max_suppression, print_args, scale_boxes, scale_segments,
                                   strip_optimizer)
        from utils.plots import Annotator, colors, save_one_box
        from utils.segment.general import masks2segments, process_mask, process_mask_native
        from utils.torch_utils import select_device, smart_inference_mode
        from utils.augmentations import letterbox

        self.model, self.stride, self.names, self.pt = self.load_model()  # 加载模型

    def load_model(self,
            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, fp16=half)
        stride, names, pt = model.stride, model.names, model.pt

        return model, stride, names, pt

    def run(self, img, imgsz=(640, 640), conf_thres=0.35, iou_thres=0.05, max_det=1000, device='', classes=None,
            agnostic_nms=False, augment=False, half=False, retina_masks=True):
        imgsz = check_img_size(imgsz, s=self.stride)  # check image size
        self.model.warmup(imgsz=(1 if self.pt else 1, 3, *imgsz))  # warmup

        cal_detect = []
        device = select_device(device)
        names = self.model.module.names if hasattr(self.model, 'module') else self.model.names  # get class names

        # Set Dataloader
        im = letterbox(img, imgsz, self.stride, self.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, proto = self.model(im, augment=augment)[:2]

        pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det, nm=32)
        # Process detections
        for i, det in enumerate(pred):  # detections per image
            annotator = Annotator(img, line_width=1, example=str(names))
            if len(det):
                # Rescale boxes from img_size to im0 size
                det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], img.shape).round()  # rescale boxes to im0 size
                masks = process_mask_native(proto[i], det[:, 6:], det[:, :4], img.shape[:2])  # HWC
                segments = [
                    scale_segments(img.shape if retina_masks else im.shape[2:], x, img.shape, normalize=True)
                    for x in reversed(masks2segments(masks))]

                # Write results
                id_list = []
                for id in range(len(det[:, :6])):
                    class_name = names[int(det[:, :6][id][5])]
                    if class_name == 'person':
                        id_list.append(id)

                def del_tensor(arr, id_list):
                    if len(id_list) == 0:
                        return arr
                    elif len(id_list) == 1:
                        arr1 = arr[:id_list[0]]
                        arr2 = arr[id_list[0] + 1:]
                        return torch.cat((arr1, arr2), dim=0)
                    else:
                        arr1 = arr[:id_list[0]]
                        arr2 = arr[id_list[0] + 1:id_list[1]]
                        arr1 = torch.cat((arr1, arr2), dim=0)
                        for id_index in range(len(id_list)):
                            arr2 = arr[id_list[id_index - 1] + 1:id_list[id_index]]
                            arr1 = torch.cat((arr1, arr2), dim=0)
                        return arr1

                det = del_tensor(det, id_list)
                masks = del_tensor(masks, id_list)

                for j, (*xyxy, conf, cls) in enumerate(reversed(det[:, :6])):
                    c = int(cls)  # integer class
                    label = f'{names[c]}'
                    contours = segments[j]
                    cal_detect.append([label, xyxy, float(conf), contours])
        return cal_detect

    def contours_in(self, contours):
        p = np.zeros(shape=(2000, 2000))
        ......

ui.py是一个使用PyQt5库创建的图形用户界面程序。该程序主要实现了一个主窗口,其中包含一个标签用于显示文本信息,以及一个按钮用于触发程序的运行。

程序的主要功能是使用YOLOv5模型进行目标检测。它通过加载模型、设置参数、运行模型推理等步骤来实现目标检测。程序还包含一些辅助函数,用于处理图像、绘制轮廓等操作。

程序的运行流程如下:

  1. 导入所需的库和模块。
  2. 定义一些全局变量和常量。
  3. 定义加载模型的函数load_model(),该函数接受一些参数,包括模型权重路径、数据集配置文件路径等,并返回加载的模型对象。
  4. 定义运行模型推理的函数run(),该函数接受一些参数,包括模型对象、输入图像、推理参数等,并返回目标检测结果。
  5. 定义处理轮廓的函数contours_in(),该函数接受一个轮廓对象,返回轮廓内部的像素坐标。
  6. 定义目标检测函数det_yolov5v6(),该函数接受一个图像路径作为输入,调用load_model()函数加载模型,然后调用run()函数进行目标检测,并返回检测结果。
  7. 定义一个继承自QThread的线程类Thread_1,该类重写了run()方法,在run()方法中调用det_yolov5v6()函数进行目标检测。
  8. 定义一个UI类Ui_MainWindow,该类包含了程序的图形界面布局和一些交互逻辑。
  9. 在UI类中,定义了一个setupUi()方法,用于设置主窗口的布局和样式。
  10. 在UI类中,定义了一个槽函数run_detection(),用于处理按钮的点击事件,创建一个Thread_1线程对象,并传入图像路径作为参数,然后启动线程。
  11. 在UI类中,定义了一个槽函数showimg(),用于显示图像。
  12. 在UI类中,定义了一个槽函数showimg2(),用于显示图像的副本。
  13. 在UI类中,定义了一个槽函数printf(),用于在标签中显示文本信息。
  14. 在UI类中,创建了一个主窗口对象,并调用setupUi()方法进行初始化。

总的来说,该程序是一个基于YOLOv5模型的目标检测应用,通过图形界面与用户交互,实现了图像的加载、目标检测和结果展示等功能。

5.3 val.py

这是一个用于在检测数据集上验证训练好的YOLOv5检测模型的程序文件。它可以加载训练好的模型权重,并在给定的数据集上进行推理。程序提供了多种使用方式,可以选择不同的模型文件格式进行推理,包括PyTorch、TorchScript、ONNX Runtime、OpenCV DNN等。程序还支持保存推理结果到文本文件或JSON文件,并可以计算模型的精度指标(如mAP)。

程序首先加载模型和数据集,并设置一些参数,如批处理大小、图像大小、置信度阈值、NMS IoU阈值等。然后,程序使用数据加载器迭代数据集中的图像,并对每个图像进行推理。推理结果可以用于计算精度指标、保存结果或绘制图像。

程序还提供了一些辅助函数,用于处理推理结果、保存结果、计算精度指标等。

总之,这个程序文件是一个用于验证YOLOv5检测模型的工具,可以在给定的数据集上进行推理,并计算模型的精度指标。

5.4 models\common.py
import torch.nn as nn

class Conv(nn.Module):
    # Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)
    default_act = nn.SiLU()  # default activation

    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = self.default_act 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
    def __init__(self, c1, c2, k=1, s=1, d=1, act=True):  # ch_in, ch_out, kernel, stride, dilation, activation
        super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), d=d, act=act)

class DWConvTranspose2d(nn.ConvTranspose2d):
    # Depth-wise transpose convolution
    def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0):  # ch_in, ch_out, kernel, stride, padding, padding_out
        super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2))

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

这个程序文件是YOLOv5的一个模块,主要包含了一些常用的模块和函数。文件中定义了一些卷积层(Conv)、深度卷积层(DWConv)、转置卷积层(DWConvTranspose2d)、Transformer层(TransformerLayer)、Transformer块(TransformerBlock)、瓶颈块(Bottleneck)、CSP瓶颈块(BottleneckCSP)、交叉卷积层(CrossConv)、C3模块(C3)、带有交叉卷积层的C3模块(C3x)、带有TransformerBlock的C3模块(C3TR)、带有SPP的C3模块(C3SPP)、带有GhostBottleneck的C3模块(C3Ghost)、空间金字塔池化层(SPP)、快速空间金字塔池化层(SPPF)、Focus层(Focus)、Ghost卷积层(GhostConv)、Ghost瓶颈块(GhostBottleneck)等。

这些模块和函数用于构建YOLOv5的网络结构,实现了不同的功能,比如卷积、池化、残差连接等。这些模块和函数可以用于构建其他的深度学习模型,提供了一些常用的功能和操作。

6.系统整体结构

整体功能和构架概述:

该项目是一个基于OpenCV和深度学习的建筑语义分割和色彩聚类分析系统。它包含了多个程序文件,用于实现不同的功能模块。主要功能包括目标检测、分类、语义分割和色彩聚类等。

程序文件功能概述:

文件路径功能概述
export.py将YOLOv5模型导出为其他格式的工具
process.py对图像进行处理,保留右半部分并调整大小
ui.py创建图形用户界面,实现图像加载和目标检测
val.py在检测数据集上验证训练好的YOLOv5模型
classify/predict.py运行YOLOv5分类推理的脚本
classify/train.py训练基于YOLOv5的分类器模型
classify/val.py验证训练好的YOLOv5分类模型在分类数据集上的表现
models/common.py包含一些常用的模块和函数,用于构建YOLOv5的网络结构

7.色彩聚类分析算法

彩色像素点的色度表示

根据颜色匹配实验,国际照明委员会(Commission Internationale de L’Eclairage,CIE)建立了一系列的色度系统.CIE 1931是一个与颜色采集、表示设备无关的色度系统.它贴近颜色匹配实验事实,不仅可以表示人眼感知到的所有颜色,还能表示颜色视觉规律,预测同一种颜色的色度坐标值范围12.由于它最贴近人眼视觉,本文采用CIE 1931系统描述图像的颜色.
为了定量描述图像颜色,首先录入CIE 1931标准色度观测者的光谱色品坐标值(2°视场,5nm间隔),生成CIE 1931色度坐标系[12;然后输入彩色图像,将像素点颜色信息从RCGB转化到CIE 1931.转化函数[a,g]= F(r, g,b)的相关细节参见文献[3].转化过程如图1所示,其中图是根据色品坐标值生成的马蹄形曲线,图1(b)为标准 Lena图像.将Lena图像的颜色信息转化到CIE 1931上,获得的色度分布如图1 ?所示.可以看到像紊点在e昂界像为内的分布疏密有致,其中色度密集分布在马蹄形光谱曲线的长波区域附近,这与Lena图像为暖色调的事实相符。

在这里插入图片描述

人眼色差与颜色聚类

人们在使用颜色空间时,马蹄形区域内每一个坐标点对应于人眼可识别的一种颜色但人眼通常难以区分色度值特别相近的颜色.实际上,色度值相近的点对应于同一种颜色知觉.1942年,麦克亚当(Macadam )设计了一个视觉实验来记录观察者对色彩变化的感知,实验结果如图2所示.
图2中的每个小椭圆区域都包含多种色度值信息,但只对应于人眼可识别的一种颜色,本文综合直方图统计方法,结合像素点的聚集规律,实现了无监督的颜色聚类,达到了自动识别图像主色的目的.
在这里插入图片描述

颜色聚类算法设计

人眼观察物体时首先捕获的是颜色.人脑对颜色信息进行分析、处理之后,形成拓扑几何特征的视觉感知,这些颜色信息就是人眼自动识别对象的基础.本文参考人眼形成视知觉的感知规律,提出了一种无监督的颜色聚类算法.

主色通常是指图像中出现概率最大的颜色,它往往对应着图像中分布面积最大或者出现最多的物体目标.为了分割出图像的主要目标,通常采用的聚类算法是有监督性的,即聚类中心的生成是随机的,这就可能导致算法在多次执行后获得的结果不同.为避免聚类结果的局部性,本文利用色度系统的全局性,统计图像中出现的所有颜色.
如图3所示,输入辣椒图(a),可获得图(b)所示的色度分布;然后根据文献[14]的分类标准,按照色纯度和主色调将色度坐标划分为若干个小区域,统计其中像素点密度最大的4个区域,提取这些区域对应的颜色,获得图?所示的4种主色.按照分布密度的大小从上到下依次排列,即主色1出现的密度最大,主色2次之,依次类推.主色3和4的色调难以区分,但原始的辣椒图中至少出现了7种颜色.通过人工判读可知,按照密度由大到小排列的4种主色依次为紫、红、绿、橙黄.多次执行聚类后的结果不变,说明文献[14]的方法能够解决局部性问题.然而,该算法是有监督性的,初始聚类数需要手动输入,可能出现颜色误分类的结果;同时,有监督的聚类容易使图像的中、小目标被忽略.因此,仅仅根据色度密度的分布还不能有效地聚类颜色.
在这里插入图片描述

为了达到理想的聚类效果,并有效地识别出图像中的小目标,需要找出像素点在色度系统中的分布规律.为此,本文建立色度直方图系统,然后利用色度直方图获得像素点的聚集规律,实现无监督的颜色聚类.
在这里插入图片描述

初始化参数Nmin = 50,S = 17,对图4所示的色度直方图进行高通滤波、区域整合,得到7个子区域.求解出这7个子区域的极大值,如图5所示.多次执行该聚类算法,获得的初始聚类中心始终不变.通过提取图4中相应的峰值,获得极大值对应的颜色信息,如图6所示.
在这里插入图片描述

8.改进头部分割网络

在开始分析分割头部网络之前,让我们先简要了解一下YOLOv5(You Only Look Once,YOLO)模型。YOLO是一种目标检测和分割模型,它在单一前向传递中同时执行目标检测和分割任务,具有高度的效率和准确性。
分割头部网络是YOLOv5模型的一部分,它负责生成墙体裂缝的分割结果。具体来说,它接受来自YOLOv5的特征图以及其他输入,并生成分割掩码,用于标识图像中的墙体裂缝区域。以下是分割头部网络的关键功能和工作原理。
在这里插入图片描述

掩码裁剪

分割头部网络使用crop_mask函数来裁剪生成的分割掩码,以保留与预测的边界框(bbox)对应的区域。这确保了生成的掩码与目标物体的位置相匹配。

掩码处理与上采样

分割头部网络使用process_mask_upsample函数来处理裁剪后的掩码。它通过与预定义的原型(protos)进行卷积操作,并对结果进行上采样,以获得最终的分割结果。这个过程可以将掩码与图像尺寸进行匹配,以便后续分析和可视化。

掩码处理与下采样

另一种处理方法是使用process_mask函数,在裁剪之前对掩码进行处理。这可以在下采样后进行,以减少计算成本。如果需要,还可以选择进行上采样,以获得更高分辨率的分割结果。

掩码的缩放

scale_image函数用于将生成的分割掩码的坐标从模型输入尺寸缩放到原始图像尺寸。这是为了确保掩码与原始图像相匹配,以便进行后续的分析和可视化。

掩码的IoU计算

在分割任务中,IoU(Intersection over Union)是一个重要的指标,用于衡量预测的掩码与真实掩码之间的重叠程度。mask_iou和masks_iou函数用于计算掩码之间的IoU,这有助于评估分割的准确性。

掩码转换为分割

最后,masks2segments函数将生成的分割掩码转换为分割线段,以便进行墙体裂缝的可视化和分析。它可以选择不同的策略来处理分割掩码,例如选择最大的分割线段或将所有分割线段合并。
在这里插入图片描述

9.训练结果分析

精度分析

在这里插入图片描述

首先,我们关注的是metrics/precision(M),metrics/recall(M),metrics/mAP_0.5(M)和metrics/mAP_0.5:0.95(M)这四个指标,它们衡量了系统对文物语义分割和色彩聚类的准确性和性能。根据数据,这四个指标在实验进行到第200个epoch时,分别为0.57907,0.60095,0.6185和0.57627。

通过观察这四个指标的变化趋势,我们可以发现,在最开始的几个epoch中,这四个指标的值都比较低,表示系统的性能较差。随着epoch的增加,这四个指标的值逐渐增加,并趋于稳定在一个相对稳定的水平。特别是在第100个epoch之后,这四个指标的值保持在一个较高的水平,说明系统在文物语义分割和色彩聚类方面取得了较好的性能。

在学习率方面,x/lr0、x/lr1和x/lr2分别表示三个学习率的值。通过观察数据,可以发现这些学习率的值在实验中保持不变,而且相对于学习率的观察,我们可以看到x/lr0、x/lr1和x/lr2在整个实验过程中的值保持不变。这可能表示系统在训练过程中使用了固定的学习率。

最后,通过对实验数据的分析,我们可以得出结论:基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统在实验中取得了不错的性能。随着epoch的增加,系统的指标值逐渐增加,并达到一个相对稳定的水平。同时,损失指标的值逐渐减小,说明系统的训练过程逐渐收敛。学习率在实验中保持不变,这可能是系统设计中使用了固定的学习率策略。

需要注意的是,我们只根据给定的实验数据进行了分析,在深入在进行深入分析之前,我们需要更多关于实验的背景信息、具体的模型架构和实验设置等。这些信息可以帮助我们更好地理解实验数据和结果。

其他数据分析

混淆矩阵(文件:confusion_matrix.png)
混淆矩阵是评估分类模型性能的重要工具。矩阵的每一行代表实际类中的实例,而每一列代表预测类中的实例。我们可以推断如下:
在这里插入图片描述

对角值:这些代表每个类别的正确预测数。值越高表示性能越好。
非对角线值:这些是错误分类。在理想的混淆矩阵中,这些都为零。
类别平衡:该矩阵还可以指示是否存在类别不平衡问题。实例数明显多于其他类的类可能会主导训练过程。
条形图(文件:labels.jpg)
条形图显示数据集中每个类的实例计数。这有助于我们了解数据集的分布以及是否存在类别不平衡。
在这里插入图片描述

类别分布:不平衡的数据集可能会导致模型有偏差,该模型可能在代表性不足的类别上表现不佳。
数据充足性:对于一些实例很少的类,模型可能无法学习足够的特征来做出准确的预测。
散点图和相关图(文件:labels_correlogram.jpg)
散点图和相关图显示不同变量之间的关系。
在这里插入图片描述

变量相关性:相关图表明每个变量如何与其他变量相关。两个变量之间的高度相关性可能表明一个变量可以预测另一个变量。
数据分布:散点图可以显示数据点在两个维度上的分布,可以突出显示聚类和异常值。

10.系统整合

下图[完整源码&数据集&环境部署视频教程&自定义UI界面]
在这里插入图片描述

参考博客《基于OpenCV和深度学习的建筑语义分割和色彩聚类分析系统》

文章来源:https://blog.csdn.net/cheng2333333/article/details/135251654
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。