边缘iou
import osfrom utils.metrics import ap_per_class, ConfusionMatrix, batch_pix_accuracy,batch_pix_accuracy_class, batch_intersection_union# 后两个新增分割import cv2"""test_custom.py与test.py的区别仅在加载器上从Cityscapes改
import argparse
import json
import os
from pathlib import Path
from threading import Thread
import numpy as np
import torch
import yaml
from tqdm import tqdm
from models.experimental import attempt_load
from utils.datasets import create_dataloader
from utils.general import coco80_to_coco91_class, check_dataset, check_file, check_img_size, check_requirements, \
box_iou, non_max_suppression, scale_coords, xyxy2xywh, xywh2xyxy, set_logging, increment_path, colorstr
from utils.metrics import ap_per_class, ConfusionMatrix, batch_pix_accuracy,batch_pix_accuracy_class, batch_intersection_union # 后两个新增分割
from utils.plots import plot_images, output_to_target, plot_study_txt
from utils.torch_utils import select_device, time_synchronized
import torch.nn.functional as F
import cv2
from models.yolo import Model
from utils.loss import ComputeLoss, SegmentationLosses, SegFocalLoss, OhemCELoss, ProbOhemCrossEntropy2d,PoseLoss
import SegmentationDataset
"""
test_custom.py与test.py的区别仅在加载器上从Cityscapes改成了Custom
新版训练测试(loader的mode为"testval")可以把验证集长边resize到base-size输入到网络, 但mask仍然是原图尺寸, 以下代码自动把网络输出双线性插值到原图算指标
调用示例:
python test.py --data cityscapes_det.yaml --segdata ./data/citys --weights ./best.pt --img-size 640 --base-size 640
即相比原版yolov5多 --segdata 和 --base-size两个参数
"""
Cityscapes_COLORMAP = [
# [128, 64, 128],
[0,0,0],
[244, 35, 232],
[0, 0, 192],
[70, 70, 70],
[102, 102, 0],
[190, 153, 153],
[153, 153, 153],
[250, 170, 30],
[220, 220, 0],
[107, 142, 35],
[152, 251, 152],
[0, 130, 180],
[220, 20, 60],
[255, 0, 0],
[0, 0, 70],
[0, 60, 100],
[0, 80, 100],
[0, 0, 230],
[119, 11, 32],
]
def label2image(pred, COLORMAP=Cityscapes_COLORMAP):
colormap = np.array(COLORMAP, dtype='uint8')
X = pred.astype('int32')
return colormap[X, :]
def _make_boundary_band(mask_bool, k=3):
# mask_bool: bool array
import cv2
kernel = np.ones((k, k), np.uint8)
mask_u8 = mask_bool.astype(np.uint8)
dil = cv2.dilate(mask_u8, kernel, iterations=1)
ero = cv2.erode(mask_u8, kernel, iterations=1)
band = np.logical_xor(dil, ero)
return band
def boundary_iou_per_image(pred_label, gt_label, num_classes, k_list=(2, 4, 8)):
# pred_label, gt_label: HxW int
biou_sum = np.zeros(len(k_list), dtype=np.float64)
biou_cnt = np.zeros(len(k_list), dtype=np.int64)
for idx, k in enumerate(k_list):
inter_total = 0
union_total = 0
valid = 0
for c in range(num_classes):
gt_c = gt_label == c
pred_c = pred_label == c
if not gt_c.any() and not pred_c.any():
continue
band_gt = _make_boundary_band(gt_c, k)
band_pred = _make_boundary_band(pred_c, k)
inter = np.logical_and(band_pred, band_gt).sum()
union = np.logical_or(band_pred, band_gt).sum()
if union == 0:
continue
inter_total += inter
union_total += union
valid += 1
if union_total > 0 and valid > 0:
biou_sum[idx] += inter_total / union_total
biou_cnt[idx] += 1
return biou_sum, biou_cnt
def seg_validation(model, n_segcls, valloader, device, half_precision=True):
# Fast test during the training
def eval_batch(model, image, target, half, k_list=(2, 4, 8)):
outputs = model(image)
# outputs = gather(outputs, 0, dim=0)
if n_segcls == 2:
pred = outputs[1][0] # 1是分割
if n_segcls == 14:
pred = outputs[1][1]
target = target.to(device, non_blocking=True)
pred = F.interpolate(pred, (target.shape[1], target.shape[2]), mode='bilinear', align_corners=True)
maskpred = F.interpolate(pred, (target.shape[1], target.shape[2]), mode='bilinear', align_corners=True)[0]
mask = label2image(maskpred.max(axis=0)[1].cpu().numpy(), Cityscapes_COLORMAP)[:, :, ::-1]
correct, labeled,total_pixel = batch_pix_accuracy_class(pred.data, target,n_segcls)
inter, union = batch_intersection_union(pred.data, target, n_segcls)
# boundary IoU per image
pred_label = pred.argmax(1).cpu().numpy()
target_np = target.cpu().numpy()
biou_sum = np.zeros(len(k_list), dtype=np.float64)
biou_cnt = np.zeros(len(k_list), dtype=np.int64)
for b in range(pred_label.shape[0]):
bs, bc = boundary_iou_per_image(pred_label[b], target_np[b], n_segcls, k_list)
biou_sum += bs
biou_cnt += bc
return correct, labeled, inter, union,total_pixel,mask, biou_sum, biou_cnt
half = device.type != 'cpu' and half_precision # half precision only supported on CUDA
if half:
model.half()
model.eval()
total_inter, total_union, total_correct, total_label = 0, 0, 0, 0
total_c = []
# boundary IoU accumulators
biou_k_list = (2, 4, 8)
biou_total = np.zeros(len(biou_k_list), dtype=np.float64)
biou_count = np.zeros(len(biou_k_list), dtype=np.int64)
tbar = tqdm(valloader, desc='\r')
numtime = 1
for i, (image, target) in enumerate(tbar):
image = image.to(device, non_blocking=True)
image = image.half() if half else image.float()
with torch.no_grad():
correct, labeled, inter, union ,total_pixel,mask, biou_sum, biou_cnt = eval_batch(model, image, target, half, k_list=biou_k_list)
#将测试集中的图片进行可视化保存
# imagename = (image[0].cpu().numpy().transpose(1,2,0) * 255).astype('uint8')
# dst_fsd = cv2.addWeighted(mask, 0.4, imagename, 0.6, 0)
# cv2.imwrite('/ai/zhdata/multiyolov5_point_v2/image/FSD/train_v30_zh_20240805_FSD/1/' + str(i) + '.jpg', dst_fsd)
#将测试集中的图片进行可视化保存
total_correct += correct
total_label += labeled
total_inter += inter
total_union += union
total_c += total_pixel
biou_total += biou_sum
biou_count += biou_cnt
pixAcc = 1.0 * total_correct / (np.spacing(1) + total_label)
total_c = np.array(total_c)
pixAcc_class = 1.0 * total_c[:,0] / (np.spacing(1) + total_c[:,1])
IoU = 1.0 * total_inter / (np.spacing(1) + total_union)
numtime += 1
mIoU = IoU.mean()
# tbar.set_description(
# 'pixAcc: %.3f, mIoU: %.3f' % (pixAcc, mIoU))
if i >= len(tbar) - 1:
print('mIoU= ',str(mIoU))
for j in range(len(IoU)):
print("class:" + str(j) + "result------" +str(IoU[j]) + "--ACC----" + str(pixAcc_class[j]))
if len(IoU) == 14:
tbar.set_description(
'Acc: %.2f, mIoU: %.2f, 0: %.2f, 1: %.2f, 2: %.2f, 3: %.2f, 4: %.2f, 5: %.2f, 6: %.2f, 7: %.2f, 8: %.2f, 9: %.2f, 10: %.2f, 11: %.2f, 12: %.2f, 13: %.2f ' % (pixAcc, mIoU, IoU[0], IoU[1], IoU[2], IoU[3], IoU[4] \
, IoU[5], IoU[6], IoU[7], IoU[8], IoU[9], IoU[10], IoU[11], IoU[12], IoU[13]))
# tbar.set_description(
# 'Acc: %.2f, Acc: %.2f, 0: %.2f, 1: %.2f, 2: %.2f, 3: %.2f, 4: %.2f, 5: %.2f, 6: %.2f, 7: %.2f, 8: %.2f, 9: %.2f, 10: %.2f, 11: %.2f, 12: %.2f, 13: %.2f ' % (pixAcc, mIoU, IoU[0], IoU[1], IoU[2], IoU[3], IoU[4] \
# , IoU[5], IoU[6], IoU[7], IoU[8], IoU[9], IoU[10], IoU[11], IoU[12], IoU[13]))
# tbar.set_description(
# 'Acc: %.2f, mIoU: %.2f, 0: %.2f, 1: %.2f, 2: %.2f, 3: %.2f, 4: %.2f, 5: %.2f, 6: %.2f ' % (pixAcc, mIoU, IoU[0], IoU[1], IoU[2], IoU[3], IoU[4] \
# , IoU[5], IoU[6]))
if len(IoU) == 2:
tbar.set_description(
'pixAcc: %.3f, mIoU: %.3f, class0: %.3f, class1: %.3f' % (pixAcc, mIoU, IoU[0], IoU[1]))
biou_mean = biou_total / np.maximum(1, biou_count)
print("Boundary IoU:", {f"@{k}": float(b) for k, b in zip(biou_k_list, biou_mean)})
return mIoU,IoU
def segtest(weights, root="data/citys", batch_size=16, half_precision=True, n_segcls=19, base_size=2048): # 会使用原始尺寸测, 未考虑尺寸对不齐, 图片尺寸应为32倍数
device = select_device(opt.device, batch_size=batch_size)
model = attempt_load(weights, map_location=device) # load FP32 model
testvalloader = SegmentationDataset.get_rm_loader(root, batch_size=batch_size, split="val", mode="val", workers=8, base_size=base_size) #get_custom_loader
# testvalloader = SegmentationDataset.get_citys_loader(root, batch_size=batch_size, split="val", mode="val", workers=4, base_size=1024, crop_size=1024)
seg_validation(model, n_segcls, testvalloader, device, half_precision)
def segtest_fsd(weights, root="data/citys", batch_size=16, half_precision=True, n_segcls=19, base_size=2048): # 会使用原始尺寸测, 未考虑尺寸对不齐, 图片尺寸应为32倍数
device = select_device(opt.device, batch_size=batch_size)
model = attempt_load(weights, map_location=device) # load FP32 model
testvalloader = SegmentationDataset.get_custom_loader(root, batch_size=batch_size, split="val", mode="val", workers=8, base_size=base_size) #get_custom_loader
# testvalloader = SegmentationDataset.get_citys_loader(root, batch_size=batch_size, split="val", mode="val", workers=4, base_size=1024, crop_size=1024)
seg_validation(model, n_segcls, testvalloader, device, half_precision)
def test(data,
weights=None,
batch_size=64,
imgsz=640,
conf_thres=0.001,
iou_thres=0.6, # for NMS
save_json=False,
single_cls=False,
augment=False,
verbose=False,
model=None,
dataloader=None,
save_dir=Path(''), # for saving images
save_txt=False, # for auto-labelling
save_hybrid=False, # for hybrid auto-labelling
save_conf=False, # save auto-label confidences
plots=False,
wandb_logger=None,
compute_loss=False,
half_precision=True,
is_coco=False):
# Initialize/load model and set device
training = model is not None
if training: # called by train.py
device = next(model.parameters()).device # get model device
else: # called directly
set_logging()
device = select_device(opt.device, batch_size=batch_size)
# Directories
save_dir = Path(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model
model = attempt_load(weights, map_location=device) # load FP32 model
# model = Model(opt.cfg, ch=3, nc=3).to('cpu') # create
gs = max(int(model.stride.max()), 32) # grid size (max stride)
imgsz = check_img_size(imgsz, s=gs) # check img_size
# Multi-GPU disabled, incompatible with .half() https://github.com/ultralytics/yolov5/issues/99
# if device.type != 'cpu' and torch.cuda.device_count() > 1:
# model = nn.DataParallel(model)
# Half
half = device.type != 'cpu' and half_precision # half precision only supported on CUDA
# half = False
if half:
model.half()
# Configure
model.eval()
if isinstance(data, str):
is_coco = data.endswith('coco.yaml')
with open(data) as f:
data = yaml.load(f, Loader=yaml.SafeLoader)
check_dataset(data) # check
nc = 1 if single_cls else int(data['nc']) # number of classes
iouv = torch.linspace(0.5, 0.95, 10).to(device) # iou vector for mAP@0.5:0.95
niou = iouv.numel()
# Logging
log_imgs = 0
if wandb_logger and wandb_logger.wandb:
log_imgs = min(wandb_logger.log_imgs, 100)
# Dataloader
if not training:
if device.type != 'cpu':
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once
task = opt.task if opt.task in ('train', 'val', 'test') else 'val' # path to train/val/test images
dataloader = create_dataloader(data[task], imgsz, batch_size, gs, opt, pad=0.5, rect=True,
prefix=colorstr(f'{task}: '))[0]
#/ai/DataSets/OD_FSD_zh/psd_v2.0/data/txt_dataset/ann_zh/test_img_list_v5.txt
#data[task]
#/ai/DataSets/OD_FSD_zh/psd_v2.0/data/83_psd_20240723_占用属性优化/alldata/test_img_list.txt
seen = 0
confusion_matrix = ConfusionMatrix(nc=nc)
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
coco91class = coco80_to_coco91_class()
s = ('%20s' + '%12s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
p, r, f1, mp, mr, map50, map, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0.
loss = torch.zeros(3, device=device)
jdict, stats, ap, ap_class, wandb_images = [], [], [], [], []
numtotal = 0
numocc,numvip,numwoman,numdisabled,numcharging = 0,0,0,0,0
for batch_i, (img, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
img = img.to(device, non_blocking=True)
img = img.half() if half else img.float() # uint8 to fp16/32
img /= 255.0 # 0 - 255 to 0.0 - 1.0
targets = targets.to(device)
nb, _, height, width = img.shape # batch size, channels, height, width
with torch.no_grad():
# Run model
t = time_synchronized()
out, train_out = model(img, augment=augment)[0] # inference and training outputs 修改[0]新模型输出[0]是检测
t0 += time_synchronized() - t
# Compute loss
if compute_loss:
# Hyperparameters 配置超参数
with open(opt.hyp) as f:
hyp = yaml.load(f, Loader=yaml.SafeLoader) # load hyps
model.hyp = hyp
model.gr = 1.0
compute_loss = PoseLoss(model)
loss += compute_loss([x.float() for x in train_out], targets)[1][:3] # box, obj, cls
# Run NMS
targets[:, 2:6] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
t = time_synchronized()
out = non_max_suppression(out, conf_thres=conf_thres, iou_thres=iou_thres, labels=lb, multi_label=False)
t1 += time_synchronized() - t
# Statistics per image
for si, pred in enumerate(out):
perd_slot = pred[:, 14:]
pred = torch.cat((pred[:, :5], pred[:, 13:14]), 1)
labels = targets[targets[:, 0] == si, 1:]
nl = len(labels)
tcls = labels[:, 0].tolist() if nl else [] # target class
tocc = labels[:, 13].tolist() if nl else []
tvip = labels[:, 14].tolist() if nl else []
twoman = labels[:, 15].tolist() if nl else []
tdisable = labels[:, 16].tolist() if nl else []
tcharging = labels[:, 17].tolist() if nl else []
path = Path(paths[si])
seen += 1
if len(pred) == 0:
if nl:
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
continue
# Predictions
predn = pred.clone()
scale_coords(img[si].shape[1:], predn[:, :4], shapes[si][0], shapes[si][1]) # native-space pred
# Append to text file
if save_txt:
gn = torch.tensor(shapes[si][0])[[1, 0, 1, 0]] # normalization gain whwh
for *xyxy, conf, cls in predn.tolist():
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(save_dir / 'labels' / (path.stem + '.txt'), 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
# W&B logging - Media Panel Plots
if len(wandb_images) < log_imgs and wandb_logger.current_epoch > 0: # Check for test operation
if wandb_logger.current_epoch % wandb_logger.bbox_interval == 0:
box_data = [{"position": {"minX": xyxy[0], "minY": xyxy[1], "maxX": xyxy[2], "maxY": xyxy[3]},
"class_id": int(cls),
"box_caption": "%s %.3f" % (names[cls], conf),
"scores": {"class_score": conf},
"domain": "pixel"} for *xyxy, conf, cls in pred.tolist()]
boxes = {"predictions": {"box_data": box_data, "class_labels": names}} # inference-space
wandb_images.append(wandb_logger.wandb.Image(img[si], boxes=boxes, caption=path.name))
wandb_logger.log_training_progress(predn, path, names) if wandb_logger and wandb_logger.wandb_run else None
# Append to pycocotools JSON dictionary
if save_json:
# [{"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}, ...
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
box = xyxy2xywh(predn[:, :4]) # xywh
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
for p, b in zip(pred.tolist(), box.tolist()):
jdict.append({'image_id': image_id,
'category_id': coco91class[int(p[5])] if is_coco else int(p[5]),
'bbox': [round(x, 3) for x in b],
'score': round(p[4], 5)})
# Assign all predictions as incorrect
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device)
if nl:
detected = [] # target indices
tcls_tensor = labels[:, 0]
# target boxes
tbox = xywh2xyxy(labels[:, 1:5])
scale_coords(img[si].shape[1:], tbox, shapes[si][0], shapes[si][1]) # native-space labels
if plots:
confusion_matrix.process_batch(predn, torch.cat((labels[:, 0:1], tbox), 1))
# Per target class
for cls in torch.unique(tcls_tensor):
ti = (cls == tcls_tensor).nonzero(as_tuple=False).view(-1) # prediction indices
pi = (cls == pred[:, 5]).nonzero(as_tuple=False).view(-1) # target indices
# Search for detections
if pi.shape[0]:
# Prediction to target ious
ious, i = box_iou(predn[pi, :4], tbox[ti]).max(1) # best ious, indices
# Append detections
detected_set = set()
for j in (ious > iouv[0]).nonzero(as_tuple=False):
d = ti[i[j]] # detected target
if d.item() not in detected_set:
detected_set.add(d.item())
detected.append(d)
correct[pi[j]] = ious[j] > iouv # iou_thres is 1xn
if len(detected) == nl: # all targets already located in image
break
# Append statistics (correct, conf, pcls, tcls)
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
# Plot images
if plots and batch_i < 3:
f = save_dir / f'test_batch{batch_i}_labels.jpg' # labels
Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start()
f = save_dir / f'test_batch{batch_i}_pred.jpg' # predictions
Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start()
# Compute statistics
stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy
if len(stats) and stats[0].any():
p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
else:
nt = torch.zeros(1)
# Print results
pf = '%20s' + '%12i' * 2 + '%12.3g' * 4 # print format
print(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
# Print results per class
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
for i, c in enumerate(ap_class):
print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
# Print speeds
t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + (imgsz, imgsz, batch_size) # tuple
if not training:
print('Speed: %.1f/%.1f/%.1f ms inference/NMS/total per %gx%g image at batch-size %g' % t)
# Plots
if plots:
confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
if wandb_logger and wandb_logger.wandb:
val_batches = [wandb_logger.wandb.Image(str(f), caption=f.name) for f in sorted(save_dir.glob('test*.jpg'))]
wandb_logger.log({"Validation": val_batches})
if wandb_images:
wandb_logger.log({"Bounding Box Debugger/Images": wandb_images})
# Save JSON
if save_json and len(jdict):
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
anno_json = '../coco/annotations/instances_val2017.json' # annotations json
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
print('\nEvaluating pycocotools mAP... saving %s...' % pred_json)
with open(pred_json, 'w') as f:
json.dump(jdict, f)
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json) # init annotations api
pred = anno.loadRes(pred_json) # init predictions api
eval = COCOeval(anno, pred, 'bbox')
if is_coco:
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files] # image IDs to evaluate
eval.evaluate()
eval.accumulate()
eval.summarize()
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
except Exception as e:
print(f'pycocotools unable to run: {e}')
# Return results
model.float() # for training
if not training:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''
print(f"Results saved to {save_dir}{s}")
maps = np.zeros(nc) + map
for i, c in enumerate(ap_class):
maps[c] = ap[i]
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='test.py')
parser.add_argument('--cfg', type=str, default='models/yolov5s_custom_seg.yaml', help='model.yaml path')#
parser.add_argument('--hyp', type=str, default='data/hyp.scratch.yaml', help='hyperparameters path')
#runs/train/exp52/weights/exp52_last_114_v2.0.21_20241129.pt
#runs/train/exp4/weights/best.pt
#runs/train/exp52_zh/weights/exp52_last_115_20250311_v2.0.25.pt
#runs/train/exp54/weights/exp55_last_104_20250430_v2.0.27.pt
#runs/train/exp54/weights/exp54_last_118_20250530_v2.0.28.pt
#runs/train/exp54/weights/exp54_last_118_20250607_v2.0.29.pt
#runs/train/exp54/weights/exp54_last_118_20250619_v2.0.30.pt
#runs/train/exp54/weights/exp54_last_118_20250714_v2.0.31.pt
#runs/train/exp54/weights/exp54_last_114_20250731_v2.0.32.pt
#runs/train/exp54/weights/exp54_last_118_20250812_v2.0.33.pt
#runs/train/exp54/weights/exp54_last_111_20250912_v2.0.34.pt
#runs/train/exp54/weights/exp54_last_115_20250912_v2.0.34.pt
parser.add_argument('--weights', nargs='+', type=str, default='runs/train/exp2/weights/last_97.pt', help='model.pt path(s)') #'runs/train/exp51/weights/last.pt'
parser.add_argument('--data', type=str, default='data/custom.yaml', help='*.data path')
# fsd
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v30_20250909_gt.txt'] # v2.0.34
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v29_20250806_gt.txt'] # v2.0.33
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v28_20250726_gt.txt'] # v2.0.32
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v28_20250708_gt.txt'] # v2.0.31
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v27_20250607_gt.txt'] # v2.0.30
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v27_20250607_gt.txt'] # v2.0.29
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v26_20250506_gt.txt'] # v2.0.28
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_cpp_v25_20250422_gt.txt'] # v2.0.27 cpp
# path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_zh_v23_20250312_gt.txt'] # v2.0.25 zh
path = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/freeSpace/annotations/ann_zh/val_zh_v23_20250305_gt.txt']
# rm
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v37_20250909_gt.txt'] # v2.0.34
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v36_20250806_gt.txt'] # v2.0.33
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v35_20250726_gt.txt'] # v2.0.32
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v35_20250708_gt.txt'] # v2.0.31
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v34_20250613_gt.txt'] # v2.0.30
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v34_20250607_gt.txt'] # v2.0.29
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v33_20250525_gt.txt'] # v2.0.28
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v32_20250414_gt.txt'] # v2.0.27 cpp
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v30_20250305_gt.txt'] # v2.0.25 zh
path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/ann_zh/new_dataset/val_v30_20250305_gt.txt']
# path_rm = ['/ai/DataSets/TopViewMultiTaskPerc_xmlin/roadmarking/annotations/20240411_生态园and保定对向路沿/2task_rm_20240405_wuluhong/2task_rm_20240405_wuluhong_gt.txt']
parser.add_argument('--segdata', default=path, help='root path of segmentation data')#type=list,
parser.add_argument('--segdata_rm', default=path_rm, help='root path of segmentation data')#type=list,
parser.add_argument('--batch-size', type=int, default=64, help='size of each image batch')
parser.add_argument('--img-size', type=int, default=608, help='inference size (pixels)')
parser.add_argument('--base-size', type=int, default=736, help='long side of segtest image you want to input network')
parser.add_argument('--conf-thres', type=float, default=0.001, help='object confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS')
parser.add_argument('--task', default='val', help='train, val, test, speed or study')
parser.add_argument('--device', default='0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--verbose', action='store_true', help='report mAP by class')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-json', action='store_true', help='save a cocoapi-compatible JSON results file')
parser.add_argument('--project', default='runs/test', help='save to project/name')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
opt = parser.parse_args()
opt.save_json |= opt.data.endswith('coco.yaml')
opt.data = check_file(opt.data) # check file
print(opt)
check_requirements()
if opt.task in ('train', 'val', 'test'): # run normally
print(1111)
test(opt.data,
opt.weights,
opt.batch_size,
opt.img_size,
opt.conf_thres,
opt.iou_thres,
opt.save_json,
opt.single_cls,
opt.augment,
opt.verbose,
save_txt=opt.save_txt | opt.save_hybrid,
save_hybrid=opt.save_hybrid,
save_conf=opt.save_conf,
)
elif opt.task == 'speed': # speed benchmarks
for w in opt.weights:
test(opt.data, w, opt.batch_size, opt.img_size, 0.25, 0.45, save_json=False, plots=False)
elif opt.task == 'study': # run over a range of settings and save/plot
# python test.py --task study --data coco.yaml --iou 0.7 --weights yolov5s.pt yolov5m.pt yolov5l.pt yolov5x.pt
x = list(range(256, 1536 + 128, 128)) # x axis (image sizes)
for w in opt.weights:
f = f'study_{Path(opt.data).stem}_{Path(w).stem}.txt' # filename to save to
y = [] # y axis
for i in x: # img-size
print(f'\nRunning {f} point {i}...')
r, _, t = test(opt.data, w, opt.batch_size, i, opt.conf_thres, opt.iou_thres, opt.save_json,
plots=False)
y.append(r + t) # results and times
np.savetxt(f, y, fmt='%10.4g') # save
os.system('zip -r study.zip study_*.txt')
plot_study_txt(x=x) # plot
segtest_fsd(root=opt.segdata, weights=opt.weights, batch_size=64, n_segcls=2, base_size=opt.base_size) # 19 for cityscapes
segtest(root=opt.segdata_rm, weights=opt.weights, batch_size=64, n_segcls=14, base_size=opt.base_size) # 19 for cityscapes
可行,而且正是针对边缘对齐的一类经典指标(通常叫 Boundary IoU / Boundary F-score / BIoU)。要点如下:
- 思路:只在 GT 与预测的“边界带”上计算 IoU(或 F-score),弱化内部大面积正确带来的高分,突出边缘对齐精度。
- 典型做法(与此前的 trimap 类似):
1) 对 GT mask 做形态学梯度或膨胀-腐蚀差,得到边界带;预测 mask 同理。
2) 也可以:对 GT 做膨胀 k 像素得到 gt_dil,再做腐蚀 k 像素得到 gt_ero,取 gt_band = gt_dil XOR gt_ero(或 gt_dil - gt_ero),预测同法。
3) 计算 BIoU = |band_pred ∩ band_gt| / |band_pred ∪ band_gt|。
4) k(边带宽度)通常取 2/4/8 像素多档,报告一行表格:BIoU@2/4/8。
5) 多类别时,对每个类别各算一遍(基于该类的二值掩码),再宏/微平均。
- 与全图 mIoU 的关系:
- 全图 mIoU 对大面积区域友好;BIoU 专门惩罚边缘错位。两者结合可以明确“是不是只在边缘掉点”。
- 实际落地建议(在你的 test_custom.py 的 seg_validation 内):
- 已经有预测插值回原图:取 pred_label = pred.argmax(1)、target。
- 写一个 make_band(mask, k) 返回 bool 边带;写一个 boundary_iou(pred_band, gt_band)。
- 循环 k=2,4,8 记录 BIoU_k 的总和与计数,最后打印均值。
- 可选:同一批次保存可视化(原图叠加 GT 边界=红,Pred 边界=绿)方便肉眼校验。
- 适用性与注意事项:
- 动态分辨率下,插值误差会放大边缘偏移,BIoU能显著体现;建议固定 align_corners=True(你当前已用)保持一致性。
- 边界很细时,k 不宜过大;多档能避免单一 k 偏见。
- 若类别极不平衡,宏平均比微平均更公平。
结论:边缘 IoU(BIoU)是可行且推荐的边缘专项指标,能直观量化动态分辨率对边缘对齐的影响,建议与全图 mIoU 同时报告。
已在 test_custom.py 增加 Boundary IoU 计算并打印:
- 新增 _make_boundary_band 与 boundary_iou_per_image,对预测/GT 的各类别边界做 XOR 得到边带,计算交并比。
- 在 seg_validation 内,每个 batch 计算并累计 BIoU(默认 k=2/4/8 像素),循环结束后输出 Boundary IoU: {'@2': ..., '@4': ..., '@8': ...}。
- 现有 mIoU/PixAcc 流程不变。
使用方式:按原流程运行 test_custom.py,终端最后会多打印一行 Boundary IoU,方便对比边缘对齐表现。
详细解释一下你的边缘iou是如何计算的?
下面按代码实现解释 Boundary IoU 的计算流程:
- 先为每个类别分别取“边界带”:对该类的二值掩码做膨胀和腐蚀,二者异或得到边带 band(仅含边缘像素)。这一步在 _make_boundary_band 完成。
- 对同一类别的预测边带 band_pred 与 GT 边带 band_gt,计算交并比 |band_pred ∩ band_gt| / |band_pred ∪ band_gt|,这是单类的 BIoU。对没有 GT 和预测的类别跳过。
- 对一个图像,遍历所有类别并累加交、并,然后得到该图像在每个 k(边带宽度)下的平均 BIoU。k 多档(默认 2/4/8 像素),分别求值。
- 在验证循环中,按批次把各图像的 BIoU 累加求平均,最终打印 Boundary IoU: {'@2': x, '@4': y, '@8': z}。
相关代码位置示例:
99:145:test_custom.py
def _make_boundary_band(mask_bool, k=3):
import cv2
kernel = np.ones((k, k), np.uint8)
mask_u8 = mask_bool.astype(np.uint8)
dil = cv2.dilate(mask_u8, kernel, iterations=1)
ero = cv2.erode(mask_u8, kernel, iterations=1)
band = np.logical_xor(dil, ero)
return band
def boundary_iou_per_image(pred_label, gt_label, num_classes, k_list=(2, 4, 8)):
biou_sum = np.zeros(len(k_list), dtype=np.float64)
biou_cnt = np.zeros(len(k_list), dtype=np.int64)
for idx, k in enumerate(k_list):
inter_total = 0
union_total = 0
valid = 0
for c in range(num_classes):
gt_c = gt_label == c
pred_c = pred_label == c
if not gt_c.any() and not pred_c.any():
continue
band_gt = _make_boundary_band(gt_c, k)
band_pred = _make_boundary_band(pred_c, k)
inter = np.logical_and(band_pred, band_gt).sum()
union = np.logical_or(band_pred, band_gt).sum()
if union == 0:
continue
inter_total += inter
union_total += union
valid += 1
if union_total > 0 and valid > 0:
biou_sum[idx] += inter_total / union_total
biou_cnt[idx] += 1
return biou_sum, biou_cnt
146:205:test_custom.py
def seg_validation(...):
def eval_batch(..., k_list=(2, 4, 8)):
...
pred_label = pred.argmax(1).cpu().numpy()
target_np = target.cpu().numpy()
biou_sum = np.zeros(len(k_list), dtype=np.float64)
biou_cnt = np.zeros(len(k_list), dtype=np.int64)
for b in range(pred_label.shape[0]):
bs, bc = boundary_iou_per_image(pred_label[b], target_np[b], n_segcls, k_list)
biou_sum += bs
biou_cnt += bc
return ..., biou_sum, biou_cnt
...
biou_k_list = (2, 4, 8)
biou_total = np.zeros(len(biou_k_list), dtype=np.float64)
biou_count = np.zeros(len(biou_k_list), dtype=np.int64)
...
correct, ..., biou_sum, biou_cnt = eval_batch(..., k_list=biou_k_list)
...
biou_total += biou_sum
biou_count += biou_cnt
biou_mean = biou_total / np.maximum(1, biou_count)
print("Boundary IoU:", {f"@{k}": float(b) for k, b in zip(biou_k_list, biou_mean)})
总结:BIoU 只看边界带的对齐程度,多档宽度 k,按类别求交并比后平均,再在整个验证集上求均值输出。
我是初学者,听不太懂,可以更详细地解释下吗,我想先从宏观的角度理解下,然后再帮我解释细节吧
下面先用直观类比,再讲简化步骤,最后再补充细节。
宏观理解(用比喻)
- 想像把 GT 和预测的“涂色”边缘描出来,各自是一条线。
- 只在“边缘线”附近比对重合程度:重合越多、错位越少,分数越高;线条重叠得越好,BIoU 越接近 1。
- 和普通 mIoU 不同:mIoU 更看整体填充是否对(大块区域占主要权重),BIoU 只盯边缘,不让大片内部“遮羞”。
简化步骤(不看代码也能复现的逻辑)
1) 拿到预测分割图和 GT(每个像素的类别)。
2) 把每个类别的“边缘”找出来:用形态学膨胀和腐蚀,二者做异或,得到一圈薄薄的“边带”。GT 做一次,预测做一次。
3) 只对这两条“边带”算 IoU:交集面积 / 并集面积。交并都是边带像素数量。
4) 多类别时,逐类计算,再平均。
5) 边带的厚度 k 可以调(如 2/4/8 像素),厚度越大,容忍的偏移越多;报告时一般多档一起看。
细节解释(对应代码里做的事)
- 边带生成 _make_boundary_band(mask_bool, k):
- mask_bool 是某一类的二值图。
- 膨胀(让区域外扩 k/2)、腐蚀(让区域内缩 k/2),两者异或 -> 只剩下边缘一圈。
- 单图、多类的 BIoU boundary_iou_per_image:
- 对每个 k(2/4/8)循环;
- 对每个类别取预测/GT 边带;如果该类 GT 和预测都没有像素,跳过;
- 计算交、并(都是边带像素数);如果并为 0 跳过;
- 交/并 累加后求平均,得到该图在该 k 下的 BIoU。
- 全集汇总(在 seg_validation):
- 每个 batch 累加 BIoU 总和和计数;
- 最终 biou_mean = biou_total / biou_count,打印如 Boundary IoU: {'@2': 0.xx, '@4': 0.xx, '@8': 0.xx}。
怎么解读结果
- 越接近 1 越好;与全图 mIoU 对比,如果 mIoU 高但 BIoU 低,说明边缘偏移/毛刺问题明显。
- 看不同 k:
- 小 k 低、大 k 高,说明轻微偏移;
- 各档都低,说明边缘预测整体质量差或锯齿明显。
你可以如何使用
- 直接运行 test_custom.py,终端末尾会多出 Boundary IoU 多档结果。
- 如果只想大致看边缘,关注 @2 或 @4;如果想容忍一些偏移,看看 @8。
- 与 mIoU 一起报告,能快速判断“整体对了但边缘糟糕”的情况。
更多推荐
所有评论(0)