vmaf介绍
-
VMAF,即Video Muitimethod Assessment Fusion(视频多方法评价融合);由 Netflix 推出的视频质量评价工具,用来解决传统指标不能反映多种场景、多种特征的视频情况。该指标是目前互联网视频最主流的客观视频评价指标,适用于衡量大规模环境中流播视频质量的观感。
-
vmaf是一种视频质量指标,将人类视觉建模与机器学习相结合,由Netflix和南加州大学C.-C Jay Kuo教授之间的研究合作;之后德克萨斯大学奥斯汀分校Alan Bovik教授和南特大学Patrick Le Callet教授之间展开合作,用来提高与人类主观感知有关的vmaf准确性,并扩大其范围涵盖更多用例;2016年6月完成开源。
-
vmaf可以用作优化准则,以获得更好的编码决策;VMAF被用于我们的整个生产流程中,不仅可以测量编码过程的结果,还可以指导编码达到最佳质量。在我们的动态优化器中,有一个关于如何在编码中使用VMAF的重要示例 Dynamic Optimizer,其中,每镜头的编码决策由每个编码器选项的比特率和质量测量确定。在优化过程中,VMAF 分数对于获取准确的质量测量以及选择 凸壳上的最终分辨率/比特率点至关重要。
-
VMAF分数范围从 0 到 100,其中 0 表示最低质量,100表示最高。思考VMAF分数的一种好方法是将其线性映射到人的意见量表,在此条件下可获得训练分数。例如,默认模型 v0.6.1 使用由绝对类别评分(ACR)方法收集的分数进行训练,该分数使用 1080p 显示屏,观看距离为3H。观看者对视频质量的评分为“很差”,“差”,“一般”,“好”和“优秀”,并且粗略地讲,“差”被映射为VMAF级别20,“优秀”为100。
-
视频数据源的内容特征:
○ 动漫、室内、室外、镜头摇移、面部拉近、人物、水面、显著的物体、多个物体;
○ 不同亮度、不同对比度、不同材质、不同活动、颜色变化、色泽浓郁度、锐利度;
○ 块效应、振铃效应、胶片颗粒度、蚊式噪声;
编译
https://blog.csdn.net/yanceyxin/article/details/108075460
原理
- 三种指标:视觉信息保真度(VIF:visual quality fidelity)、细节损失指标(DLM:detail loss measure)、时域运动指标/平均相关位置像素差(TI:temporal information)。其中VIF和DLM是空间域的,一帧画面之内的特征。TI 是时间域的,多帧画面之间相关性的特征。这些特性之间融合计算总分的过程使用了训练好的SVM来预测。
- VIF:视觉信息保真度指标来源于论文《image information and visual quality》https://ieeexplore.ieee.org/document/1576816。它是一种基于自然场景统计模型NSS、图像失真、和人类视觉失真建模的新判断;该指标认为人眼看到的图像是图像通过HVS过滤出来的信息,HVS本身就是一个失真通道,即人类视觉失真通道,而失真图像只是比原始图像在经过HVS之前又多了一个图像失真通道,故可以使用信息论的知识将人眼提取的信息与从原始图像提取的信息进行比较,得出最终评测结果。
- DLM:细节损失指标来源于论文《Image Quality Assessment by Separately Evaluating Detail Losses and Additive Impairments》https://ieeexplore.ieee.org/document/5765502。该算法分别评估细节损失(Detail Loss Measure, DLM)和附加损伤(Additive Impairment Measure, AIM)。细节损失是指影响内容可视性的有用视觉信息的损失,加性损伤是指多余的视觉信息,即在测试图像中出现的分散观众对有用内容的注意力的信息,从而导致不好的观看体验。为了分离细节损失和加性损伤,首先将原始参考图像和被测图像通过一种小波域解耦算法进行分离,分离出小波变换系数O、修复图像的小波变换系数R以及加性损伤的小波变换系数A。经过对比敏感度函数和对比掩膜效应函数两种近似HVS敏感度的特性处理后,得到了O’、R’、A’,再通过两种简单的质量测量方法,得到两种质量结果q1和q2,最后将细节损失和附加损伤两个质量度量的输出与视觉质量相关联,得到总体质量指数s。在VMAF中只使用细节损失q1作为基本指标,但也对一些特殊情况采取了必要的措施,如导致原始公式中的数值计算失效的黑帧。
- TI :时域运动指标/平均相关位置像素差是一种衡量相邻帧之间时域差分的算法;这个仅仅计算像素亮度分量的均值作差即可得到该值。
CAMBI【新加入的算法,检测条带失真】
新版本增加的检测条带失真;netfix用传统的、非神经网络(NNN,non-neural network)的方法设计了一种算法来满足我们的要求,具体如下:
模型分析
- HDTV模式:针对客厅电视场景设计,参照 SMPTE EG-18-1994 标准,所有训练集的主观数据 (EMOS) 是遵循这种方式收集的:将视频 scale 成 1080p,并以三倍于屏幕高度 (3H) 的观看距离或 60 像素/度的角度分辨率进行显示。可以说,VMAF 模型试图捕捉的是 3H 外显示的 1080p 视频的感知质量。这是默认 VMAF 模型的隐含假设。因此,我们在对除 1080p 以外的其他分辨率进行 VMAF 计算时,需要将视频先 scale 成1080p,才能保证结果的准确性。试想一下,如果将一个 480p 的视频用 HDTV 模式来做评测,会发生什么情况呢?这就好像 480p 视频是从 1080p 视频中剪切出来的一样。如果 480p 视频的高度为 H ',则 H = 1080 / 480 * H ',其中 H 为所显示的 1080p 视频的高度。因此,VMAF 建模的是 3H = 6.75H ’ 的观看距离。换句话说,如果你计算 480p 分辨率的视频对的 VMAF,你将预测观看距离是其高度的 6.75 倍时的感知质量。在此观看距离会隐藏大量主观画质的感知细节,从而让 VMAF 分数可能偏高。
- phone模式:默认的 vmaf_float_v0.6.1.pkl 模型也提供了移动设备场景的画质评估功能,可以通过参数 “–phone-model” 开启。在此模式下,每个受试者在他/她感到舒适的距离观看视频。在训练过的模型中,分数在 0 - 100 之间,与主观投票量表呈线性关系,粗略的 “bad” 映射为20分,“excellent” 映射为 100 分。横向对比可以发现,如果将一部 540p 的视频分别放在标准电视、手机设备、4K 设备显示,在手机设备上的 VMAF 质量会比其他两者更快的逼近 100 分的临界值。
- 4K模式:由于 vmaf_float_v0.6.1.pkl 模型的训练集中包括了 4K 和 1080p 的视频源,当需要对比 A/B 两个 4K 视频哪一个画质更好时,也能使用此模型。但是由于默认模型采用的是 1080p + 3H 观看距离的方式采集的 EMOS 数据,无法算出准确的 4K VMAF 分数。Netflix 后来专门提供了 vmaf_4k_v0.6.1.pkl 用于 4K 的画质评估。
API介绍
//初始化,分配并打开vmaf实例
int vmaf_init(VmafContext**vmaf, VmafConfigurationcfg);
//注册特定的“VmafModel”所需的特征提取器。这可以使用不同的模型调用多次。在这种情况下,注册的特征提取器将形成一个集合,多个模型所需的特征只提取一次
int vmaf_use_features_from_model(VmafContext*vmaf, VmafModel*model);
//注册特定的“VmafModelCollection”所需的特征提取器,可以多次使用
int vmaf_use_features_from_model_collection(VmafContext*vmaf, VmafModelCollection*model_collection);
//注册特定特征提取器,当需要一个特定的/附加的特性时(比如psnr、ssim、msssim等),通常是一个 不是已经由模型通过' vmaf_use_features_from_model() '提供的。
int vmaf_use_feature(VmafContext*vmaf, constchar*feature_name, VmafFeatureDictionary*opts_dict);
//导入外部特性评分。当预先计算的功能分数可用时很有用。 在所需特性没有libvmaf特性提取器实现的情况下也很有用。
int vmaf_import_feature_score(VmafContext*vmaf, constchar*feature_name, doublevalue, unsignedindex);
//读一组图片,并将它们排队等待最终的特征提取。这应该在特征提取器通过' vmaf_use_features_from_model() '和/或' vmaf_use_feature() '注册后调用。' VmafContext '将获得' VmafPicture ' (' ref ' and ' dist ')和' vmaf_picture_unref() '的所有权。
int vmaf_read_pictures(VmafContext*vmaf,VmafPicture*ref,VmafPicture*dist, unsignedindex);
//预测具体指标的VMAF得分。
int vmaf_score_at_index(VmafContext*vmaf, VmafModel *model,double*score, unsignedindex);
//使用模型集合预测特定指数的VMAF得分。
int vmaf_score_at_index_model_collection(VmafContext*vmaf, VmafModelCollection*model_collection, VmafModelCollectionScore*score, unsignedindex);
//获取特定索引的特性评分。
int vmaf_feature_score_at_index(VmafContext*vmaf,constchar*feature_name, double*score,unsignedindex);
//指定间隔的VMAF评分池。
int vmaf_score_pooled(VmafContext*vmaf, VmafModel *model,enumVmafPoolingMethodpool_method,double*score,unsignedindex_low,unsignedindex_high);
//使用模型集合的特定间隔的池VMAF评分。
int vmaf_score_pooled_model_collection(VmafContext*vmaf,VmafModelCollection*model_collection,enumVmafPoolingMethodpool_method,VmafModelCollectionScore*score,
unsignedindex_low,unsignedindex_high);
//在特定的时间间隔内的汇集特性得分。
int vmaf_feature_score_pooled(VmafContext*vmaf,constchar*feature_name, enumVmafPoolingMethodpool_method,double*score, unsignedindex_low,unsignedindex_high);
//关闭一个VMAF实例并释放所有相关内存。
int vmaf_close(VmafContext*vmaf);
//将VMAF统计数据写入输出文件。
int vmaf_write_output(VmafContext*vmaf,constchar*output_path, enumVmafOutputFormatfmt);
compute_vmaf demo代码逻辑分析
command line tool源码分析
[vmaf.c]
int main(int argc, char *arg[])
{
int err = 0;
//文件流相关操作
const int istty = isatty(fileno(stderr));
//解析命令行,传入CLISettings结构体中
CLISettings c;
cli_parse(argc, argv, &c);
//输出VMAF版本号
if (istty && !c.quiet) {
fprintf(stderr, "VMAF version %s\n", vmaf_version());
}
//从路径读入视频数据
FILE *file_ref = fopen(c.path_ref, "rb");
if (!file_ref) {
fprintf(stderr, "could not open file: %s\n", c.path_ref);
return -1;
}
FILE *file_dist = fopen(c.path_dist, "rb");
if (!file_dist) {
fprintf(stderr, "could not open file: %s\n", c.path_dist);
return -1;
}
//从FILE中传入video_input结构体里
video_input vid_ref;
if (c.use_yuv) {
err = raw_input_open(&vid_ref, file_ref,
c.width, c.height, c.pix_fmt, c.bitdepth);
} else {
err = video_input_open(&vid_ref, file_ref);
}
if (err) {
fprintf(stderr, "problem with reference file: %s\n", c.path_ref);
return -1;
}
video_input vid_dist;
if (c.use_yuv) {
err = raw_input_open(&vid_dist, file_dist,
c.width, c.height, c.pix_fmt, c.bitdepth);
} else {
err = video_input_open(&vid_dist, file_dist);
}
if (err) {
fprintf(stderr, "problem with distorted file: %s\n", c.path_dist);
return -1;
}
//验证ref和dist视频的相容性
err = validate_videos(&vid_ref, &vid_dist);
if (err) {
fprintf(stderr, "videos are incompatible, %d %s.\n",
err, err == 1 ? "problem" : "problems");
return -1;
}
//configure信息
VmafConfiguration cfg = {
.log_level = VMAF_LOG_LEVEL_INFO,
.n_threads = c.thread_cnt,
.n_subsample = c.subsample,
.cpumask = c.cpumask,
};
//1. 初始化,vmaf是上下文,cfg是选择结构体用来初始化上下文
VmafContext *vmaf;
err = vmaf_init(&vmaf, cfg);
if (err) {
fprintf(stderr, "problem initializing VMAF context\n");
return -1;
}
//内存申请
VmafModel **model;
const size_t model_sz = sizeof(*model) * c.model_cnt;
model = malloc(model_sz);
memset(model, 0, model_sz);
VmafModelCollection **model_collection;
const size_t model_collection_sz =
sizeof(*model_collection) * c.model_cnt;
model_collection = malloc(model_sz);
memset(model_collection, 0, model_collection_sz);
const char *model_collection_label[c.model_cnt];
unsigned model_collection_cnt = 0;
//对model次数进行循环操作
for (unsigned i = 0; i < c.model_cnt; i++) {
//根据判断version是否使用来调用不同的方法load模型
if (c.model_config[i].version) {
//加载默认内置模型之一
err = vmaf_model_load(&model[i], &c.model_config[i].cfg,
c.model_config[i].version);
} else {
//从系统文件读取模型文件
err = vmaf_model_load_from_path(&model[i], &c.model_config[i].cfg,
c.model_config[i].path);
}
//加载模型错误的情况下
if (err) {
// check for model_collection before failing
// this is implicit because the `--model` option could take either
// a model or model_collection
if (c.model_config[i].version) {
err = vmaf_model_collection_load(&model[i],
&model_collection[model_collection_cnt],
&c.model_config[i].cfg,
c.model_config[i].version);
} else {
err = vmaf_model_collection_load_from_path(&model[i],
&model_collection[model_collection_cnt],
&c.model_config[i].cfg,
c.model_config[i].path);
}
if (err) {
fprintf(stderr, "problem loading model: %s\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path);
return -1;
}
model_collection_label[model_collection_cnt] =
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path;
for (unsigned j = 0; j < c.model_config[i].overload_cnt; j++) {
err = vmaf_model_collection_feature_overload(
model[i],
&model_collection[model_collection_cnt],
c.model_config[i].feature_overload[j].name,
c.model_config[i].feature_overload[j].opts_dict);
if (err) {
fprintf(stderr,
"problem overloading feature extractors from "
"model collection: %s\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path);
return -1;
}
}
err = vmaf_use_features_from_model_collection(vmaf,
model_collection[model_collection_cnt]);
if (err) {
fprintf(stderr,
"problem loading feature extractors from "
"model collection: %s\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path);
return -1;
}
model_collection_cnt++;
continue;
}
//
for (unsigned j = 0; j < c.model_config[i].overload_cnt; j++) {
err = vmaf_model_feature_overload(model[i],
c.model_config[i].feature_overload[j].name,
c.model_config[i].feature_overload[j].opts_dict);
if (err) {
fprintf(stderr,
"problem overloading feature extractors from "
"model: %s\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path);
return -1;
}
}
//使用' vmaf_use_features_from_model() '注册模型所需的所有特征提取器
err = vmaf_use_features_from_model(vmaf, model[i]);
if (err) {
fprintf(stderr,
"problem loading feature extractors from model: %s\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path);
return -1;
}
}
//如果有辅助指标,使用‘vmaf_use_feature()’直接注册
for (unsigned i = 0; i < c.feature_cnt; i++) {
err = vmaf_use_feature(vmaf, c.feature_cfg[i].name,
c.feature_cfg[i].opts_dict);
if (err) {
fprintf(stderr, "problem loading feature extractor: %s\n",
c.feature_cfg[i].name);
return -1;
}
}
//循环读取所有图像
float fps = 0.;
const time_t t0 = clock();
unsigned picture_index;
for (picture_index = 0 ;; picture_index++) {
if (c.frame_cnt && picture_index >= c.frame_cnt)
break;
//核心就是封装了vmaf_picture_alloc,要分配“VmafPicture”,请使用“vmaf_picture_alloc”。分配之后,可以用像素数据填充缓冲区。
VmafPicture pic_ref, pic_dist;
int ret1 = fetch_picture(&vid_ref, &pic_ref);
int ret2 = fetch_picture(&vid_dist, &pic_dist);
if (ret1 && ret2) {
break;
} else if (ret1 < 0 || ret2 < 0) {
fprintf(stderr, "\nproblem while reading pictures\n");
break;
} else if (ret1) {
fprintf(stderr, "\n\"%s\" ended before \"%s\".\n",
c.path_ref, c.path_dist);
break;
} else if (ret2) {
fprintf(stderr, "\n\"%s\" ended before \"%s\".\n",
c.path_dist, c.path_ref);
break;
}
if (istty && !c.quiet) {
if (picture_index > 0 && !(picture_index % 10)) {
fps = (picture_index + 1) /
(((float)clock() - t0) / CLOCKS_PER_SEC);
}
fprintf(stderr, "\r%d frame%s %s %.2f FPS\033[K",
picture_index + 1, picture_index ? "s" : " ",
spinner[picture_index % spinner_length], fps);
fflush(stderr);
}
//使用' vmaf_read_pictures() '循环读取所有输入的图片。当您完成图片读取时,一些特征提取器可能仍然需要刷新内部缓冲区
err = vmaf_read_pictures(vmaf, &pic_ref, &pic_dist, picture_index);
if (err) {
fprintf(stderr, "\nproblem reading pictures\n");
break;
}
}
if (istty && !c.quiet)
fprintf(stderr, "\n");
//一旦缓冲区被刷新,所有对' vmaf_read_pictures() '的进一步调用都无效。
err |= vmaf_read_pictures(vmaf, NULL, NULL, 0);
if (err) {
fprintf(stderr, "problem flushing context\n");
return err;
}
//在图片被读取后,您可以检索一个vmaf分数。使用' vmaf_score_at_index '来获取单个索引的分数,并使用' vmaf_score_pooled() '来获得跨多个帧的分数
if (!c.no_prediction) {
for (unsigned i = 0; i < c.model_cnt; i++) {
double vmaf_score;
err = vmaf_score_pooled(vmaf, model[i], VMAF_POOL_METHOD_MEAN,
&vmaf_score, 0, picture_index - 1);
if (err) {
fprintf(stderr, "problem generating pooled VMAF score\n");
return -1;
}
if (istty && (!c.quiet || !c.output_path)) {
fprintf(stderr, "%s: %f\n",
c.model_config[i].version ?
c.model_config[i].version : c.model_config[i].path,
vmaf_score);
}
}
//使用模型集合预测特定指数的VMAF得分
for (unsigned i = 0; i < model_collection_cnt; i++) {
VmafModelCollectionScore score = {
0 };
err = vmaf_score_pooled_model_collection(vmaf, model_collection[i],
VMAF_POOL_METHOD_MEAN, &score,
0, picture_index - 1);
if (err) {
fprintf(stderr, "problem generating pooled VMAF score\n");
return -1;
}
switch (score.type) {
case VMAF_MODEL_COLLECTION_SCORE_BOOTSTRAP:
if (istty && (!c.quiet || !c.output_path)) {
fprintf(stderr, "%s: %f, ci.p95: [%f, %f], stddev: %f\n",
model_collection_label[i],
score.bootstrap.bagging_score, score.bootstrap.ci.p95.lo,
score.bootstrap.ci.p95.hi,
score.bootstrap.stddev);
}
break;
default:
break;
}
}
}
//输出结果到指定路径
if (c.output_path)
vmaf_write_output(vmaf, c.output_path, c.output_fmt);
//销毁、释放实例与内存
for (unsigned i = 0; i < c.model_cnt; i++)
vmaf_model_destroy(model[i]);
free(model);
for (unsigned i = 0; i < model_collection_cnt; i++)
vmaf_model_collection_destroy(model_collection[i]);
free(model_collection);
video_input_close(&vid_ref);
video_input_close(&vid_dist);
//清除vmaf
vmaf_close(vmaf);
cli_free(&c);
return err;
}
vmaf.c逻辑原理图
使用案例
- FFmpeg
- Elecard StreamEye
- MSU Video Quality Measurement Tool