视频帧的黑、花屏的检测是视频质量检测中比较重要的一部分,传统做法是由测试人员通过肉眼来判断视频中是否有黑、花屏的现象,这种方式不仅耗费人力且效率较低。为了进一步节省人力、提高效率,一种自动的检测方法是大家所期待的。目前,通过分类网络模型对视频帧进行分类来自动检测是否有黑、花屏是比较可行且高效的。然而,在项目过程中,视频帧数据的收集比较困难,数据量较少,部分花屏和正常屏之间差异不够明显,导致常用的分类算法难以满足项目对分类准确度的要求。因此本文尝试了一种利用目标检测算法实现分类的方式,帮助改善单纯的分类的算法效果不够理想的问题。

核心技术与架构图

一般分类任务的流程如下图,首先需要收集数据,构成数据集;并为每一类数据定义一个类型标签,例如:0、1、2;再选择一个合适的分类网络进行分类模型的训练,图像分类的网络有很多,常见的有VggNet, ResNet,DenseNet等;最后用训练好的模型对新的数据进行预测,输出新数据的类别。

一种基于目标检测实现黑花屏分类任务的方案_数据

目标检测任务的流程不同于分类任务,其在定义类别标签的时候还需要对目标位置进行标注;目标检测的方法也有很多,例如Fast R-CNN, SSD,YOLO等;模型训练的中间过程也比分类模型要复杂,其输出一般为目标的位置、目标置信度以及分类结果。

一种基于目标检测实现黑花屏分类任务的方案_测试_02

由于分类算法依赖于一定量的数据,在项目实践中,数据量较少或图像类间差异较小时,传统分类算法效果不一定能满足项目需求。这时,不妨考虑用目标检测的方式来做‘分类’。接下来以Yolov5为例来介绍如何将目标检测框架用于实现单纯的分类任务。

技术实现

除了分类之外,目标检测还可以从自然图像中的大量预定义类别中识别出目标实例的位置。大家可能会考虑目标检测模型用于分类是不是过于繁琐或者用目标检测框架来做单纯的分类对代码的修改比较复杂。这里,我们将用一种非常简单的方式直接在数据标注和输出内容上稍作修改就能实现单纯的分类了。接下来将介绍一下具体实现方法:

no.1

数据的标注

OBJECT_DICT = {"Normalscreen": 0, "Colorfulscreen": 1, "Blackscreen": 2}
def parse_json_file(image_path):
imageName = os.path.basename(image_path).split('.')[0]
img = cv2.imread(image_path)
size = img.shape
label = image_path.split('/')[4].split('\\')[0]
label = OBJECT_DICT.get(label)
imageWidth = size[0]
imageHeight = size[1]
label_dict = {}
xmin, ymin = (0, 0)
xmax, ymax = (imageWidth, imageHeight)
xcenter = (xmin + xmax) / 2
xcenter = xcenter / float(imageWidth)
ycenter = (ymin + ymax) / 2
ycenter = ycenter / float(imageHeight)
width = ((xmax - xmin) / float(imageWidth))
heigt = ((ymax - ymin) / float(imageHeight))
label_dict.update({label: [str(xcenter), str(ycenter), str(width), str(heigt)]})
label_dict = sorted(label_dict.items(), key=lambda x: x[0])
return imageName, label_dict

no.2

训练过程

# 加载数据,获取训练集、测试集图片路径
with open(opt.data) as f:
data_dict = yaml.load(f, Loader=yaml.FullLoader)
with torch_distributed_zero_first(rank):
check_dataset(data_dict)
train_path = data_dict['train']
test_path = data_dict['val']
Number_class, names = (1, ['item']) if opt.single_cls else (int(data_dict['nc']), data_dict['names'])

# 创建模型
model = Model(opt.cfg, ch=3, nc=Number_class).to(device)

# 学习率的设置
lf = lambda x: ((1 + math.cos(x * math.pi / epochs)) / 2) * (1 - hyp['lrf']) + hyp['lrf']
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)

# 训练

no.3

损失的计算

def compute_loss(p, targets, model):
device = targets.device
loss_cls, loss_box, loss_obj = torch.zeros(1, device=device), torch.zeros(1, device=device), torch.zeros(1, device=device)
tcls, tbox, indices, anchors = build_targets(p, targets, model)
h = model.hyp
# 定义损失函数
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device)
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device)
cp, cn = smooth_BCE(eps=0.0)
# 损失
nt = 0
np = len(p)
balance = [4.0, 1.0, 0.4] if np == 3 else [4.0, 1.0, 0.4, 0.1]
for i, pi in enumerate(p):
image, anchor, gridy, gridx = indices[i]
tobj = torch.zeros_like(pi[..., 0], device=device)
n = image.shape[0]
if n:
nt += n # 计算目标
ps = pi[anchor, image, gridy, gridx]
pxy = ps[:, :2].sigmoid() * 2. - 0.5
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]
predicted_box = torch.cat((pxy, pwh), 1).to(device) giou = bbox_iou(predicted_box.T, tbox[i], x1y1x2y2=False, CIoU=True)
loss_box += (1.0 - giou).mean()
tobj[image, anchor, gridy, gridx] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype)
if model.nc > 1:
t = torch.full_like(ps[:, 5:], cn, device=device)
t[range(n), tcls[i]] = cp
loss_cls += BCEcls(ps[:, 5:], t)
loss_obj += BCEobj(pi[..., 4], tobj) * balance[i]
s = 3 / np
loss_box *= h['giou'] * s
loss_obj *= h['obj'] * s * (1.4 if np == 4 else 1.)
loss_cls *= h['cls'] * s
bs = tobj.shape[0]
loss

no.4

对输出内容的处理

def detect(opt,img):
out, source, weights, view_img, save_txt, imgsz = \
opt.output, img, opt.weights, opt.view_img, opt.save_txt, opt.img_size
device = select_device(opt.device)
half = device.type != 'cpu'
model = experimental.attempt_load(weights, map_location=device)
imgsz = check_img_size(imgsz, s=model.stride.max())
if half:
model.half()
img = letterbox(img)[0]
img = img[:, :, ::-1].transpose(2, 0, 1)
img = np.ascontiguousarray(img)
img_warm = torch.zeros((1, 3, imgsz, imgsz), device=device)
_ = model(img_warm.half() if half else img_warm) if device.type != 'cpu' else None
img = torch.from_numpy(img).to(device)
img = img.half() if half else img.float()
img /= 255.0
if img.ndimension() == 3:
img = img.unsqueeze(0)
pred = model(img, augment=opt.augment)[0]
# 应用非极大值抑制
pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, classes=opt.classes, agnostic=opt.agnostic_nms)
# 处理检测的结果
for i, det in enumerate(pred):
if det is not None and len(det):
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img.shape).round()
all_conf = det[:, 4]
if len(det[:, -1]) > 1:
ind = torch.max(all_conf, 0)[1]
c = torch.take(det[:, -1], ind)
detect_class = int(c)
else:
for c in det[:, -1]:
detect_class

效果展示

为了将视频帧进行黑、花屏分类,测试人员根据经验将屏幕分为正常屏(200张)、花屏(200张)和黑屏(200张)三类,其中正常屏幕标签为0,花屏的标签为1,黑屏的标签为2。

一种基于目标检测实现黑花屏分类任务的方案_目标检测_03

为了进一步说明该方法的有效性,我们将基于Yolov5的‘分类’效果与ResNet分类效果做了对比。根据测试人员对ResNet分类效果的反馈来看,ResNet模型容易将正常屏与花屏错误分类,例如,下图被测试人员定义为正常屏:

一种基于目标检测实现黑花屏分类任务的方案_分类算法_04

ResNet的分类结果为1,即为花屏,显然,这不是我们想要的结果。

一种基于目标检测实现黑花屏分类任务的方案_分类算法_04

基于Yolov5的分类结果为0,即为正常屏,这是我们所期待的结果。

一种基于目标检测实现黑花屏分类任务的方案_数据_06

同时,通过对一批测试数据的分类效果来看,Yolov5的分类效果比ResNet的分类准确度更高,ResNet的分类准确率为88%,而基于Yolov5的分类准确率高达97%。

总 结

对于较小数据集的黑、花屏的分类问题,采用Yolov5来实现分类相较于ResNet的分类效果会更好一些。当我们在做图像分类任务时,纯粹的分类算法不能达到想要的效果时,不妨尝试一下用目标检测框架来分类吧!虽然过程稍微复杂一些,但可能会有不错的效果。目前目标检测框架有很多,用它们完成分类任务的处理方式大致和本文所描述的类似,可以根据数据集的特征选择合适目标检测架构来实现分类。本文主要介绍了如何将现有的目标检测框架直接用于单纯的图像分类任务,当然,为了使得结构更简洁,也可以将目标检测中的分类网络提取出来用于分类。