1.研究背景与意义
随着科技的不断发展,数字图像处理技术在文物保护和研究领域中扮演着越来越重要的角色。文物是人类文化遗产的重要组成部分,它们承载着丰富的历史、文化和艺术信息。因此,对文物进行准确的分析和保护至关重要。
传统的文物分析方法主要依赖于人工的目视观察和分析,这种方法存在着主观性强、效率低、易受人为因素影响等问题。而基于计算机视觉和图像处理技术的文物分析方法可以克服这些问题,提高分析的准确性和效率。
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习算法,它在图像识别和分析领域取得了巨大的成功。CNN可以通过学习大量的图像样本,自动提取图像的特征,并进行分类和分割。在文物分析中,CNN可以用于文物的语义分割,即将文物图像中的不同部分进行分割和标记,从而实现对文物的精细分析。
OpenCV是一个开源的计算机视觉库,它提供了丰富的图像处理和分析工具。OpenCV可以用于图像的预处理、特征提取、图像分割等任务。结合CNN和OpenCV,可以构建一个强大的文物分析系统,实现文物的语义分割和色彩聚类分析。
基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统具有以下几个方面的意义:
-
提高文物分析的准确性:传统的文物分析方法往往依赖于人工的主观判断,容易受到人为因素的影响。而基于CNN和OpenCV的文物分析系统可以自动提取文物图像的特征,并进行准确的分割和标记,提高分析的准确性。
-
提高文物分析的效率:传统的文物分析方法需要大量的人力和时间,效率低下。而基于CNN和OpenCV的文物分析系统可以自动进行图像处理和分析,大大提高了分析的效率。
-
保护文物的完整性:文物是人类文化遗产的重要组成部分,保护文物的完整性是文物研究和保护的重要任务。基于CNN和OpenCV的文物分析系统可以帮助鉴定文物的真伪和完整性,为文物保护提供科学依据。
-
推动文物研究的发展:文物研究是了解人类历史和文化的重要途径。基于CNN和OpenCV的文物分析系统可以为文物研究提供更多的数据和信息,推动文物研究的发展。
综上所述,基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统在文物保护和研究领域具有重要的意义。它可以提高文物分析的准确性和效率,保护文物的完整性,推动文物研究的发展。这对于传承和弘扬人类文化遗产,促进人类文明的发展具有重要的意义。
2.图片演示
3.视频演示
基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统_哔哩哔哩_bilibili
4.数据集的采集&标注和整理
图片的收集
首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集CulturalrelicDatasets。
eiseg是一个图形化的图像注释工具,支持COCO和YOLACT格式。以下是使用eiseg将图片标注为COCO格式的步骤:
(1)下载并安装eiseg。
(2)打开eiseg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的JSON文件。
(6)重复此过程,直到所有的图片都标注完毕。
由于YOLACT使用的是txt格式的标注,我们需要将VOC格式转换为YOLACT格式。可以使用各种转换工具或脚本来实现。
下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLACT所需的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 benchmarks.py
class Benchmarks:
def __init__(self, weights, imgsz, batch_size, data, device, half, test, pt_only, hard_fail):
self.weights = weights
self.imgsz = imgsz
self.batch_size = batch_size
self.data = data
self.device = device
self.half = half
self.test = test
self.pt_only = pt_only
self.hard_fail = hard_fail
def run(self):
y, t = [], time.time()
device = select_device(self.device)
model_type = type(attempt_load(self.weights, fuse=False)) # DetectionModel, SegmentationModel, etc.
for i, (name, f, suffix, cpu, gpu) in export.export_formats().iterrows(): # index, (name, file, suffix, CPU, GPU)
try:
assert i not in (9, 10), 'inference not supported' # Edge TPU and TF.js are unsupported
assert i != 5 or platform.system() == 'Darwin', 'inference only supported on macOS>=10.13' # CoreML
if 'cpu' in device.type:
assert cpu, 'inference not supported on CPU'
if 'cuda' in device.type:
assert gpu, 'inference not supported on GPU'
# Export
if f == '-':
w = self.weights # PyTorch format
else:
w = export.run(weights=self.weights, imgsz=[self.imgsz], include=[f], device=device, half=self.half)[-1] # all others
assert suffix in str(w), 'export failed'
# Validate
if model_type == SegmentationModel:
result = val_seg(self.data, w, self.batch_size, self.imgsz, plots=False, device=device, task='speed', half=self.half)
metric = result[0][7] # (box(p, r, map50, map), mask(p, r, map50, map), *loss(box, obj, cls))
else: # DetectionModel:
result = val_det(self.data, w, self.batch_size, self.imgsz, plots=False, device=device, task='speed', half=self.half)
metric = result[0][3] # (p, r, map50, map, *loss(box, obj, cls))
speed = result[2][1] # times (preprocess, inference, postprocess)
y.append([name, round(file_size(w), 1), round(metric, 4), round(speed, 2)]) # MB, mAP, t_inference
except Exception as e:
if self.hard_fail:
assert type(e) is AssertionError, f'Benchmark --hard-fail for {name}: {e}'
LOGGER.warning(f'WARNING ⚠️ Benchmark failure for {name}: {e}')
y.append([name, None, None, None]) # mAP, t_inference
if self.pt_only and i == 0:
break # break after PyTorch
# Print results
LOGGER.info('\n')
self.parse_opt()
notebook_init() # print system info
c = ['Format', 'Size (MB)', 'mAP50-95', 'Inference time (ms)']
py = pd.DataFrame(y, columns=c)
LOGGER.info(f'\nBenchmarks complete ({time.time() - t:.2f}s)')
LOGGER.info(str(py))
if self.hard_fail and isinstance(self.hard_fail, str):
metrics = py['mAP50-95'].array # values to compare to floor
floor = eval(self.hard_fail) # minimum metric floor to pass, i.e. = 0.29 mAP for YOLOv5n
assert all(x > floor for x in metrics if pd.notna(x)), f'HARD FAIL: mAP50-95 < floor {floor}'
return py
def parse_opt(self):
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'bests.pt', help='weights path')
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
parser.add_argument('--batch-size', type=int, default=1, help='batch size')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
parser.add_argument('--test', action='store_true', help='test exports only')
parser.add_argument('--pt-only', action='store_true', help='test PyTorch only')
parser.add_argument('--hard-fail', nargs='?', const=True, default=False, help='Exception on error or < min metric')
opt = parser.parse_args()
opt.data = check_yaml(opt.data) # check YAML
print_args(vars(opt))
return opt
封装的类为YOLACTBenchmarks,其中包含了run和parse_opt两个方法。
这个程序文件是用于运行YOLACT的基准测试的。它支持多种导出格式,并可以测试不同格式的模型的推理性能。
程序文件首先导入了必要的库和模块,并定义了一些全局变量。然后定义了两个函数run()
和test()
,分别用于运行基准测试和导出测试。这两个函数都接受一些参数,如权重路径、推理尺寸、批处理大小等。
run()
函数首先选择设备并加载模型。然后遍历所有支持的导出格式,依次进行导出和验证。导出过程中会根据不同的格式选择不同的导出方法,并检查导出是否成功。验证过程中会根据模型类型选择不同的验证方法,并计算性能指标。最后将结果保存在一个DataFrame中,并打印出来。
test()
函数与run()
函数类似,但只进行导出测试,不进行验证。
最后,程序文件定义了一个parse_opt()
函数用于解析命令行参数,一个main()
函数用于根据参数选择运行run()
或test()
函数,并在程序文件最后调用main()
函数来执行程序。
6.系统整体结构
整体功能和构架概述:
该项目是一个基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统。它包含了多个程序文件,每个文件负责不同的功能模块。主要功能包括目标检测、图像分割、分类、训练和验证等。
以下是每个文件的功能概述:
文件路径 | 功能概述 |
---|---|
benchmarks.py | 运行YOLACT的基准测试,支持多种导出格式 |
detect.py | 使用YOLACT模型进行目标检测的程序,提供GUI界面 |
export.py | 将YOLACT模型导出为其他格式的程序 |
hubconf.py | 加载和使用YOLACT模型的Python脚本 |
test.py | 对矩阵进行操作的测试程序 |
train.py | 训练YOLACT模型的程序,支持自定义数据集和分布式训练 |
ui.py | 提供用户界面的程序 |
val.py | 对YOLACT模型进行验证的程序 |
classify/predict.py | 使用分类模型进行预测的程序 |
classify/train.py | 训练分类模型的程序 |
classify/val.py | 对分类模型进行验证的程序 |
models/common.py | 包含YOLACT模型的通用函数和类 |
models/experimental.py | 包含YOLACT模型的实验性函数和类 |
models/tf.py | 包含YOLACT模型的TensorFlow实现 |
models/yolo.py | 包含YOLACT模型的主要实现 |
models/init.py | 模型初始化文件 |
segment/predict.py | 使用图像分割模型进行预测的程序 |
segment/train.py | 训练图像分割模型的程序 |
segment/val.py | 对图像分割模型进行验证的程序 |
utils/activations.py | 包含激活函数的实现 |
utils/augmentations.py | 包含数据增强的实现 |
utils/autoanchor.py | 包含自动锚框的实现 |
utils/autobatch.py | 包含自动批处理的实现 |
utils/callbacks.py | 包含训练过程中的回调函数的实现 |
utils/dataloaders.py | 包含数据加载器的实现 |
utils/downloads.py | 包含下载数据集和模型的实现 |
utils/general.py | 包含通用的辅助函数和类 |
utils/loss.py | 包含损失函数的实现 |
utils/metrics.py | 包含评价指标的实现 |
utils/plots.py | 包含绘图函数的实现 |
utils/torch_utils.py | 包含与PyTorch相关的辅助函数和类 |
utils/triton.py | 包含与Triton Inference Server相关的辅助函数和类 |
utils/init.py | 工具函数的初始化文件 |
utils/aws/resume.py | 包含AWS训练恢复的实现 |
utils/aws/init.py | AWS相关的初始化文件 |
utils/flask_rest_api/example_request.py | 包含Flask REST API的示例请求 |
utils/flask_rest_api/restapi.py | 包含Flask REST API的实现 |
utils/loggers/init.py | 日志记录器的初始化文件 |
utils/loggers/clearml/clearml_utils.py | 包含ClearML日志记录器的实现 |
utils/loggers/clearml/hpo.py | 包含ClearML超参数优化的实现 |
utils/loggers/comet/comet_utils.py | 包含Comet日志记录器的实现 |
utils/loggers/comet/hpo.py | 包含Comet超参数优化的实现 |
utils/loggers/wandb/log_dataset.py | 包含WandB日志记录器的数据集日志实现 |
utils/loggers/wandb/sweep.py | 包含WandB超参数优化的实现 |
utils/loggers/wandb/wandb_utils.py | 包含WandB日志记录器的实现 |
utils/loggers/wandb/init.py | WandB相关的初始化文件 |
utils/segment/augmentations.py | 包含图像分割的数据增强实现 |
utils/segment/dataloaders.py | 包含图像分割的数据加载器实现 |
utils/segment/general.py | 包含图像分割的通用函数和类 |
utils/segment/loss.py | 包含图像分割的损失函数实现 |
utils/segment/metrics.py | 包含图像分割的评价指标实现 |
utils/segment/plots.py | 包含图像分割的绘图函数实现 |
utils/segment/init.py | 图像分割的初始化文件 |
这些文件共同构成了基于卷积神经网络和OpenCV的文物语义分割和色彩聚类分析系统的功能模块。每个文件负责不同的功能,如模型训练、推理、数据处理。
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.YOLACT文物图像语义分割
类比Mask R-CNN之于Faster R-CNN,YOLACT旨在现有的one-stage型检测器上添加一个mask分支来达到实例分割的目的,但这—过程中不希望引入特征定位步骤。
YOLACT通过添加两个并行的分支来完成该任务:第一个分支使用FCN去产生一系列独立于单一实例的原型mask;第二个分支在检测分支上添加额外的头去预测mask系数,以用于编码一个实例在原型mask空间的表示。最后,在NMS步骤后,通过将两分支的输出结果进行线性组合来得到最后的预测结果。
YOLACT的网络结构如下图所示。
因为分割任务的目标是得到mask,而mask的特点是存在天然的空间联系,所以YOLACT采用了上述组织形式。从NN的角度来说,Conv层天然利用了空间相关性,但FC层不会。这就导致了一个问题,因为大多数One-stage检测器通过FC层预测box参数和所属类别。Two-stage通过ROl Aign等特征定位步骤保留了空间信息,同时使用Conv层输出mask,但是这些操作都必须等待RPN来完成,极大地影响了效率。
在YOLACT中,FC层负责预测语义标签,Conv层负责预测原型mask和mask系数。两分支并行,最后通过矩阵乘法组装,这样来既保留了空间的相关性,又保持了One-stage的模型结构,速度极快。
称生成原型的网络分支为protonet。protonet基于FCN实现,最后会输出k个通道,每个通道可以视作一张原型mask,protonet的作用有些类似语义分割模型,不同之处在于protonet部分的训练不单独设置loss,只在整个网络最后输出的mask上进行监督。
Emergent Behavior
YOLACT取得的效果可能有点出人意料,因为围绕实例分割任务的一个共识是:因为FCNs是平移不变的,所以需要在模型中添加转移方差。因此,在Mask R-CNN和FCIS中,通过显式方法添加了转移方差:方向图、位置存档,或是把mask预测分支放在第二个stage,都使得它们不需要再处理定位问题。
在YOLACT,唯一算是添加转移方差的地方是使用预测框裁剪feature map时。但其实这只是为了改善对小目标的分割效果,作者发现对大中型目标,不裁剪效果就很好了。所以,YOLACT似乎通过其原型的不同激活学习到了如何定位目标。
怎么理解YOLACT隐式学习到了转移方差?
对于上图a,使用无padding的FCNs是得不到的,因为输入图像处处相同,卷积权值又共享,那么输出肯定也一样。作者认为,像ResNet这样现代的FCNs通过连续的padding0,使得其具有隐式学习图像边界距离的能力。所以
ResNet隐含了转移方差于
其中,YOLACT得益于此。如图b和图c,明显不同的feature map对不同位置的目标具有不同的响应。
许多原型mask只在图像的某些部分上激活,即它们只激活位于隐式学习边界一侧的对象。例如,上图中原型6学习的是背景信息。通过对这些原型进行组合,网络可以区分同一语义的不同(甚至重叠)的实例,比如在图d中,原型4减去原型5,可以区分开红色伞和绿色伞。
此外,原型学习是可压缩的。也就是说,如果protonet将多个原型的功能合并成一个,那么mask系数分支就会去对应学习相应的组合方法。例如,上图中,原型4具有分割的能力,但同时它又对左下角部分图像具有较高响应,原型5也类似,但它就对右下角部分响应更大。这也就解释了为什么可以根据实际情况调整原型数量(即protonet的输出通道数,默认为32),而又不会带来模型性能的下降。
Backbone Detector
因为预测一组原型mask和mask系数是一个相对比较困难的任务,需要更丰富更高级的特征,所以在网络设计上,作者希望兼顾速度和特征丰富度。因此,YOLACT的主干检测器设计遵循了RetinaNet的思想,同时更注重速度。
9.训练结果分析
数据可视化分析
损失分析
训练损失(train/box_loss, train/seg_loss, train/obj_loss, train/cls_loss):这些值代表在训练过程中,模型在不同任务上的损失值,包括未知损失、分割损失、目标损失和分类损失。
import matplotlib.pyplot as plt
# Plotting the training and validation losses
plt.figure(figsize=(15, 10))
# Training losses
plt.subplot(2, 2, 1)
plt.plot(data['epoch'], data['train/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['train/seg_loss'], label='Segmentation Loss')
plt.plot(data['epoch'], data['train/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['train/cls_loss'], label='Class Loss')
plt.title('Training Losses')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# Validation losses
plt.subplot(2, 2, 2)
plt.plot(data['epoch'], data['val/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['val/seg_loss'], label='Segmentation Loss')
plt.plot(data['epoch'], data['val/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['val/cls_loss'], label='Class Loss')
plt.title('Validation Losses')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.tight_layout()
plt.show()
除了这四个指标,还有其他指标如val/box_loss、val/seg_loss、val/obj_loss和val/cls_loss等,它们分别表示验证集中边界框损失、分割损失、目标损失和分类损失。这些指标的值也随着epoch的增加而逐渐减小,说明系统的训练过程逐渐收敛并获得更好的性能。
精度分析
首先,我们关注的是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界面