本文主要介绍基于OpenCV contrib中的quality模块实现图像质量评价。图像质量评估Image Quality Analysis简称IQA,主要通过数学度量方法来评价图像质量的好坏。

本文需要OpenCV contrib库,OpenCV contrib库的编译安装见:

OpenCV_contrib库在windows下编译使用指南

本文所有代码见:

OpenCV-Practical-Exercise

1 OpenCV中图像质量评价算法介绍

1.1 相关背景

图像质量评价(IQA)算法以任意图像作为输入,输出质量分数作为输出。有三种类型的IQA:

  1. 全参考图像质量评价,适用情形:一个“干净”参考(非扭曲)图像以衡量扭曲图像的质量。此度量可用于评估图像压缩算法的质量。
  2. 半参考图像质量评价,适用情形:如果没有参考图像,而是具有一些选择性信息的图像(例如,水印图像)来比较和测量失真图像的质量。
  3. 无参考图像质量评价,适用情形:算法得到的唯一输入是要测量其质量的图像。

在OpenCV contrib的quality模块中一共有提供了5种图像质量评价算法,按上面的类别分仅提供全参考图像质量评价和无参考图像质量评价两种类别的算法,没有半参考图像质量评价算法。官方代码地址见quality,其中包含的5种图像质量评价算法具体如下:

  • 均方误差 Mean squared error (MSE)
  • 峰值信噪比 Peak signal-to-noise ratio (PSNR)
  • 结构相似性 Structural similarity (SSIM)
  • 梯度幅度相似性偏差 Gradient Magnitude Similarity Deviation (GMSD)
  • 盲/无参考图像空间质量评估器 Blind/Referenceless Image Spatial Quality Evaluation (BRISQUE)

这5种图像质量评价算法中,除了BRISQUE是无参考图像质量评价算法外,其他都是全参考图像质量评价。本文不具体介绍这些算法的原理,仅介绍这些算法的应用。想知道具体原理见链接:

事实上,各种图像质量评估算法都是寻找不同数学公式给出一个评判结果,差异并不那么大,仅知道使用即可。就全参考图像质量评价算法而言,一般情况下GMSD效果比其他全参考图像质量评价算法效果好。无参考图形质量评价以BRISQUE为代表。半参考图像质量评价更多用于发论文,实际应用不多。近年来也有深度学习应用于图像质量评估,但是效果还不错,但速度太慢。关于图像质量评估算法具体进一步研究可参考链接:图像质量评估指标(Image Quality Assessment,IQA)

1.2 OpenCV中图像质量评价算法接口介绍

OpenCV中图像质量评价算法接口分为静态方法和实例方法,静态方法固定快捷,实例方法灵活性强。其中全参考图像质量评价算法接口类似,只需要更改函数名即可,因为各种参考图像质量算法其实都数学公式应用变换数学公式即可。BRISQUE在[OpenCV实战]37 图像质量评价BRISQUE中已经提到如何使用,不过用起来相对opencv_contrib库中的quality模块麻烦,唯一好处[OpenCV实战]37 图像质量评价BRISQUE提到的方法不需要编译opencv_contrib库,但是实际建议使用opencv_contrib库的quality模块来实现图像质量评估算法。

1.2.1 opencv_contrib中全参考图像质量评价算法具体接口

C++/静态方法

// output quality map
// 质量结果图
// 质量结果图quality_map就是检测图像和基准图像各个像素点差值图像
cv::Mat quality_map;
// compute MSE via static method
// cv::noArray() if not interested in output quality maps
// 静态方法,一步到位
// 如果不想获得质量结果图,将quality_map替换为noArray()
cv::Scalar result_static = quality::QualityMSE::compute(img1, img2, quality_map);

C++/实例方法

// alternatively, compute MSE via instance
cv::Ptr<quality::QualityBase> ptr = quality::QualityMSE::create(img1);
// compute MSE, compare img1 vs img2
cv::Scalar result = ptr->compute(img2);
ptr->getQualityMap(quality_map);

Python/静态方法

# 静态方法,一步到位
# 质量结果图quality_map就是检测图像和基准图像各个像素点差值结果
result_static, quality_map = cv2.quality.QualityMSE_compute(img1, img2)

Python/实例方法

obj = cv2.quality.QualityMSE_create(img1)
result = obj.compute(img2)
quality_map = obj.getQualityMap()

1.2.2 opencv_contrib中无参考图像质量评价算法具体接口

C++/静态方法

// path to the trained model
cv::String model_path = "./model/brisque_model_live.yml";
// path to range file
cv::String range_path = "./model/brisque_range_live.yml";
// 静态计算方法
cv::Scalar result_static = quality::QualityBRISQUE::compute(img, model_path, range_path);

C++/实例方法

cv::Ptr<quality::QualityBase> ptr = quality::QualityBRISQUE::create(model_path, range_path);
// computes BRISQUE score for img
cv::Scalar result = ptr->compute(img)

Python/静态方法

# path to the trained model
model_path = "./model/brisque_model_live.yml"
# path to range file
range_path = "./model/brisque_range_live.yml"
# 静态计算方法
result_static = cv2.quality.QualityBRISQUE_compute(img, model_path, range_path)

Python/实例方法

obj = cv2.quality.QualityBRISQUE_create(model_path, range_path)
result = obj.compute(img)

1.2.3 opencv_contrib中图像质量评价算法输出参数介绍

对于静态方法和实例方法输出结果一样的,都是输出在不同颜色通道下的结果,比如对于全参考图像质量评价算法而言RGB图就是分别输出R、G、B三个通道的结果,所以最后需要求均值。对BRISQUE而言不管是彩色图还是灰度图都只输出一个0到100之间的数。各个算法的结果特点如下表所示:

算法 输出结果特点
MSE 结果越小,检测图像和基准图像的差距越小
PSNR 结果越小,检测图像和基准图像的差距越大
GMSD 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越大
SSIM 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越小
BRISQUE 结果为一个0到100之间的数,越小表示检测图像质量越好

2 代码实现与结果分析

2.1 代码实现

本文所提供的代码可以对者图像进行质量评价。本文提供C++和Python代码实现,但是MSE的Python实例计算代码可能有问题,可以用Python静态方法代替,所有代码如下:

C++

#include <opencv2/opencv.hpp>
#include <opencv2/quality.hpp> using namespace std;
using namespace cv; // 计算结果均值
double calMEAN(Scalar result)
{
int i = 0;
double sum = 0;
// 计算总和
for (auto val : result.val)
{
if (0 == val || isinf(val))
{
break;
}
sum += val;
i++;
}
return sum / i;
} // 均方误差 MSE
double MSE(Mat img1, Mat img2)
{
// output quality map
// 质量结果图
// 质量结果图quality_map就是检测图像和基准图像各个像素点差值图像
cv::Mat quality_map;
// compute MSE via static method
// cv::noArray() if not interested in output quality maps
// 静态方法,一步到位
// 如果不想获得质量结果图,将quality_map替换为noArray()
cv::Scalar result_static = quality::QualityMSE::compute(img1, img2, quality_map); /* 另外一种动态计算的方法
// alternatively, compute MSE via instance
cv::Ptr<quality::QualityBase> ptr = quality::QualityMSE::create(img1);
// compute MSE, compare img1 vs img2
cv::Scalar result = ptr->compute(img2);
ptr->getQualityMap(quality_map);
*/ return calMEAN(result_static);
} // 峰值信噪比 PSNR
double PSNR(Mat img1, Mat img2)
{
// 质量结果图
// 质量结果图quality_map就是检测图像和基准图像各个像素点差值图像
cv::Mat quality_map;
// 静态方法,一步到位
// 如果不想获得质量结果图,将quality_map替换为noArray()
// 第四个参数为PSNR计算公式中的MAX,即图片可能的最大像素值,通常为255
cv::Scalar result_static = quality::QualityPSNR::compute(img1, img2, quality_map, 255.0); /* 另外一种动态计算的方法
cv::Ptr<quality::QualityBase> ptr = quality::QualityPSNR::create(img1, 255.0);
cv::Scalar result = ptr->compute(img2);
ptr->getQualityMap(quality_map);*/ return calMEAN(result_static);
} // 梯度幅度相似性偏差 GMSD
double GMSD(Mat img1, Mat img2)
{
// 质量结果图
// 质量结果图quality_map就是检测图像和基准图像各个像素点差值图像
cv::Mat quality_map;
// 静态方法,一步到位
// 如果不想获得质量结果图,将quality_map替换为noArray()
cv::Scalar result_static = quality::QualityGMSD::compute(img1, img2, quality_map);
/* 另外一种动态计算的方法
cv::Ptr<quality::QualityBase> ptr = quality::QualityGMSD::create(img1);
cv::Scalar result = ptr->compute(img2);
ptr->getQualityMap(quality_map);*/
return calMEAN(result_static);
} // 结构相似性 SSIM
double SSIM(Mat img1, Mat img2)
{
// 质量结果图
// 质量结果图quality_map就是检测图像和基准图像各个像素点差值图像
cv::Mat quality_map;
// 静态方法,一步到位
// 如果不想获得质量结果图,将quality_map替换为noArray()
cv::Scalar result_static = quality::QualitySSIM::compute(img1, img2, quality_map);
/* 另外一种动态计算的方法
cv::Ptr<quality::QualityBase> ptr = quality::QualitySSIM::create(img1);
cv::Scalar result = ptr->compute(img2);
ptr->getQualityMap(quality_map);*/
return calMEAN(result_static);
} // 盲/无参考图像空间质量评估器 BRISQUE
double BRISQUE(Mat img)
{
// path to the trained model
cv::String model_path = "./model/brisque_model_live.yml";
// path to range file
cv::String range_path = "./model/brisque_range_live.yml";
// 静态计算方法
cv::Scalar result_static = quality::QualityBRISQUE::compute(img, model_path, range_path);
/* 另外一种动态计算的方法
cv::Ptr<quality::QualityBase> ptr = quality::QualityBRISQUE::create(model_path, range_path);
// computes BRISQUE score for img
cv::Scalar result = ptr->compute(img);*/
return calMEAN(result_static);
} void qualityCompute(String methodType, Mat img1, Mat img2)
{
// 算法结果和算法耗时
double result;
TickMeter costTime; costTime.start();
if ("MSE" == methodType)
result = MSE(img1, img2);
else if ("PSNR" == methodType)
result = PSNR(img1, img2);
else if ("PSNR" == methodType)
result = PSNR(img1, img2);
else if ("GMSD" == methodType)
result = GMSD(img1, img2);
else if ("SSIM" == methodType)
result = SSIM(img1, img2);
else if ("BRISQUE" == methodType)
result = BRISQUE(img2);
costTime.stop();
cout << methodType << "_result is: " << result << endl;
cout << methodType << "_cost time is: " << costTime.getTimeSec() / costTime.getCounter() << " s" << endl;
} int main()
{
// img1为基准图像,img2为检测图像
cv::Mat img1, img2;
img1 = cv::imread("image/original-rotated-image.jpg");
img2 = cv::imread("image/noise-version.jpg"); if (img1.empty() || img2.empty())
{
cout << "img empty" << endl;
return 0;
} // 结果越小,检测图像和基准图像的差距越小
qualityCompute("MSE", img1, img2);
// 结果越小,检测图像和基准图像的差距越大
qualityCompute("PSNR", img1, img2);
// 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越大
qualityCompute("GMSD", img1, img2);
// 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越小
qualityCompute("SSIM", img1, img2);
// BRISQUE不需要基准图像
// 结果为一个0到100之间的数,越小表示检测图像质量越好
qualityCompute("BRISQUE", cv::Mat{}, img2);
system("pause");
return 0;
}

Python

# -*- coding: utf-8 -*-
"""
Created on Fri Oct 9 05:27:28 2020 @author: luohenyueji
""" import cv2
import numpy as np
import time # ----- 时间装饰器,打印运行结果和运行时间
def usetime(func):
def inner(*args, **kwargs):
time_start = time.time()
# 装饰的函数在此运行
result = func(*args, **kwargs)
time_run = time.time() - time_start
# 打印结果
print(func.__name__ + '_result is: {:.3f}'.format(result))
# 打印运行时间
print(func.__name__ + '_cost time is: {:.3f} s'.format(time_run)) return inner # ----- 均方误差 MSE
@usetime
def MSE(img1, img2):
# 静态方法,一步到位
# 质量结果图quality_map就是检测图像和基准图像各个像素点差值结果
result_static, quality_map = cv2.quality.QualityMSE_compute(img1, img2)
# 另外一种动态计算的方法,但是MSE的计算可能有问题
# obj = cv2.quality.QualityMSE_create(img1)
# result = obj.compute(img2)
# quality_map = obj.getQualityMap()
# 计算均值
score = np.mean([i for i in result_static if (i != 0 and not np.isinf(i))])
score = 0 if np.isnan(score) else score
return score # ----- 峰值信噪比 PSNR
@usetime
def PSNR(img1, img2):
# 静态方法,一步到位
# 质量结果图quality_map就是检测图像和基准图像各个像素点差值结果
# maxPixelValue参数为PSNR计算公式中的MAX,即图片可能的最大像素值,通常为255
result_static, quality_map = cv2.quality.QualityPSNR_compute(img1, img2, maxPixelValue=255)
# 另外一种动态计算的方法
# obj = cv2.quality.QualityPSNR_create(img1, maxPixelValue=255)
# result = obj.compute(img2)
# quality_map = obj.getQualityMap()
# 计算均值
score = np.mean([i for i in result_static if (i != 0 and not np.isinf(i))])
return score # ----- 梯度幅度相似性偏差 GMSD
@usetime
def GMSD(img1, img2):
# 静态方法,一步到位
# 质量结果图quality_map就是检测图像和基准图像各个像素点差值结果
result_static, quality_map = cv2.quality.QualityGMSD_compute(img1, img2)
# 另外一种动态计算的方法
# obj = cv2.quality.QualityGMSD_create(img1)
# result = obj.compute(img2)
# quality_map = obj.getQualityMap()
# 计算均值
score = np.mean([i for i in result_static if (i != 0 and not np.isinf(i))])
score = 0 if np.isnan(score) else score
return score # ----- 结构相似性 SSIM
@usetime
def SSIM(img1, img2):
# 静态方法,一步到位
# 质量结果图quality_map就是检测图像和基准图像各个像素点差值结果
result_static, quality_map = cv2.quality.QualitySSIM_compute(img1, img2)
# 另外一种动态计算的方法
# obj = cv2.quality.QualitySSIM_create(img1)
# result = obj.compute(img2)
# quality_map = obj.getQualityMap()
# 计算均值
score = np.mean([i for i in result_static if (i != 0 and not np.isinf(i))])
score = 0 if np.isnan(score) else score
return score # ----- 盲/无参考图像空间质量评估器 BRISQUE
@usetime
def BRISQUE(img):
# path to the trained model
model_path = "./model/brisque_model_live.yml"
# path to range file
range_path = "./model/brisque_range_live.yml"
# 静态计算方法
result_static = cv2.quality.QualityBRISQUE_compute(img, model_path, range_path)
# # 另外一种动态计算的方法
# obj = cv2.quality.QualityBRISQUE_create(model_path, range_path)
# result = obj.compute(img)
# 计算均值
score = np.mean([i for i in result_static if (i != 0 and not np.isinf(i))])
score = 0 if np.isnan(score) else score
return score def main():
# img1为基准图像,img2为检测图像
img1 = cv2.imread("image/cut-original-rotated-image.jpg")
img2 = cv2.imread("image/cut-noise-version.jpg")
if img1 is None or img2 is None:
print("img empty")
return
# 结果越小,检测图像和基准图像的差距越小
MSE(img1, img2)
# 结果越小,检测图像和基准图像的差距越大
PSNR(img1, img2)
# 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越大
GMSD(img1, img2)
# 结果为一个0到1之间的数,越大表示检测图像和基准图像的差距越小
SSIM(img1, img2)
# 结果为一个0到100之间的数,越小表示检测图像质量越好
BRISQUE(img2) if __name__ == '__main__':
main()

2.2 结果分析

上面的代码实现了对不同图片的图像质量诊断,并输出各种方法在不同图像下的评分和方法检测速度。速度计算主要基于C++代码。
具体检测结果如下表所示,其中-nan(ind)表示结果出错,通常是两张图像一样。原图下的结果是原图和原图比,模糊图片和噪声图片是与原图为基准图片比较的结果。按清晰度而言,原图>模糊图片>噪声图片。
下面分别显示分辨率为612x816和中心裁剪分辨率305x305的结果。

结果 原图/分辨率612x816 模糊图片/分辨率612x816 噪声图片/分辨率612x816
方法
MSE -nan(ind) 1490.28 1734.03
PSNR -nan(ind) 16.3989 15.7454
GMSD -nan(ind) 0.209512 0.199491
SSIM 1 0.30256 0.482258
BRISQUE 53.3901 63.4859 71.2059
结果 原图/分辨率305x305 模糊图片/分辨率305x305 噪声图片/分辨率305x305
方法
MSE -nan(ind) 1303.96 984.486
PSNR -nan(ind) 16.9784 18.2243
GMSD -nan(ind) 0.111176 0.113035
SSIM 1 0.30256 0.687856
BRISQUE 56.1736 42.0616 73.3258

各个方法具体检测速度如下表所示:

速度/s 原图/分辨率612x816 模糊图片/分辨率612x816 噪声图片/分辨率612x816
方法
MSE 0.029 0.021 0.020
PSNR 0.017 0.019 0.019
GMSD 0.032 0.031 0.032
SSIM 0.084 0.086 0.084
BRISQUE 0.068 0.073 0.071
速度/s 原图/分辨率305x305 模糊图片/分辨率305x305 噪声图片/分辨率305x305
方法
MSE 0.006 0.005 0.005
PSNR 0.004 0.005 0.004
GMSD 0.012 0.011 0.012
SSIM 0.025 0.031 0.033
BRISQUE 0.027 0.028 0.028

从上面的结果可以得到如下分析:

  1. 对于612x816分辨率图片,结果正确的有MSE,GMSD,BRISQUE;对于305x305分辨率图片,如果从局部上来看,噪声图片和模糊图片清晰图差不太多,结果正确的有PSNR,GMSD。然而对于BRISQUE模糊图片的清晰度评分比原图高。所以通常情况下,有参考图片,GMSD准确率最高,其他方法并不靠谱,BRISQUE需要更加完整的大图才有好的效果。
  2. 就速度而言,图像分辨率越高,各个方法耗时也越多,毕竟都是靠图像像素点差值公式计算的,不过都能在1s以内获得结果。
  3. 如果有有参图像,最好用GMSD。BRISQUE更适合高分辨率图片,如果要低分辨率使用,建议自己重新训练模型,毕竟BRISQUE的模型太老了。关于BRISQUE模型训练见:quality/samples

总而言之,现在图像质量评价算法都只能针对某种特定环境使用,在实际最好针对每一种图像噪声情况设定一种判定算法,现在各个视频检测平台也都是这样做的。如果普通使用看看GMSD和BRISQUE即可。

3 参考

3.1 参考代码

3.2 参考文章

[OpenCV实战]48 基于OpenCV实现图像质量评价的更多相关文章

  1. [OpenCV实战]47 基于OpenCV实现视觉显著性检测

    人类具有一种视觉注意机制,即当面对一个场景时,会选择性地忽略不感兴趣的区域,聚焦于感兴趣的区域.这些感兴趣的区域称为显著性区域.视觉显著性检测(Visual Saliency Detection,VS ...

  2. [OpenCV实战]45 基于OpenCV实现图像哈希算法

    目前有许多算法来衡量两幅图像的相似性,本文主要介绍在工程领域最常用的图像相似性算法评价算法:图像哈希算法(img hash).图像哈希算法通过获取图像的哈希值并比较两幅图像的哈希值的汉明距离来衡量两幅 ...

  3. [OpenCV实战]28 基于OpenCV的GUI库cvui

    目录 1 cvui的使用 1.1 如何在您的应用程序中添加cvui 1.2 基本的"hello world"应用程序 2 更高级的应用 3 代码 4 参考 有很多很棒的GUI库,例 ...

  4. [OpenCV实战]38 基于OpenCV的相机标定

    文章目录 1 什么是相机标定? 2 图像形成几何学 2.1 设定 2.1.1 世界坐标系 2.1.2 相机坐标系 2.1.3 图像坐标系 2.2 图像形成方法总结 3 基于OpenCV的相机标定原理 ...

  5. [OpenCV实战]26 基于OpenCV实现选择性搜索算法

    目录 1 背景 1.1 目标检测与目标识别 1.2 滑动窗口算法 1.3 候选区域选择算法 2 选择性搜索算法 2.1 什么是选择性搜索? 2.2 选择性搜索相似性度量 2.3 结果 3 代码 4 参 ...

  6. [OpenCV实战]51 基于OpenCV实现图像极坐标变换与逆变换

    在图像处理领域中,经常通过极坐标与笛卡尔直角坐标的互转来实现图像中圆形转为方形,或者通过极坐标反变换实现方形转圆形.例如钟表的表盘,人眼虹膜,医学血管断层都需要用到极坐标变换来实现圆转方. 文章目录 ...

  7. [OpenCV实战]11 基于OpenCV的二维码扫描器

    目录 1 二维码(QRCode)扫描 2 结果 3 参考 在这篇文章中,我们将看到如何使用OpenCV扫描二维码.您将需要OpenCV3.4.4或4.0.0及更高版本来运行代码. 1 二维码(QRCo ...

  8. [OpenCV实战]15 基于深度学习的目标跟踪算法GOTURN

    目录 1 什么是对象跟踪和GOTURN 2 在OpenCV中使用GOTURN 3 GOTURN优缺点 4 参考 在这篇文章中,我们将学习一种基于深度学习的目标跟踪算法GOTURN.GOTURN在Caf ...

  9. [OpenCV实战]5 基于深度学习的文本检测

    目录 1 网络加载 2 读取图像 3 前向传播 4 处理输出 3结果和代码 3.1结果 3.2 代码 参考 在这篇文章中,我们将逐字逐句地尝试找到图片中的单词!基于最近的一篇论文进行文字检测. EAS ...

随机推荐

  1. Docker | 容器数据卷详解

    什么是容器数据卷 从docker的理念说起,docker将应用和环境打包成一个镜像,运行镜像(生成容器)就可以访问服务了. 如果数据都存在容器中,那么删除容器,数据就会丢失!需求:数据可以持久化 My ...

  2. DDD-领域驱动(三)-聚合与聚合根

    概念 高内聚 , 高内聚合Aggregate 就好比一个功能,各个模块互相是有依赖关系存在,例如: 低耦合:模块可以任意替换,不会影响系统的工作 例如:比如你今天穿了这套衣服,明天穿了另一套衣服,但你 ...

  3. StampedLock:一个并发编程中非常重要的票据锁

    摘要:一起来聊聊这个在高并发环境下比ReadWriteLock更快的锁--StampedLock. 本文分享自华为云社区<[高并发]一文彻底理解并发编程中非常重要的票据锁--StampedLoc ...

  4. Linux自动切换用户

    Linux自动切换用户 一.创建sh文件 touch su_user.sh 二.下载脚本 yum install -y expect 三.脚本内容 #!/bin/bash# This is our f ...

  5. OpenAPI 接口幂等实现

    OpenAPI 接口幂等实现 1.幂等性是啥? 进行一次接口调用与进行多次相同的接口调用都能得到与预期相符的结果. 通俗的讲,创建资源或更新资源的操作在多次调用后只生效一次. 2.什么情况会需要保证幂 ...

  6. 基于mnist的P-R曲线(准确率,召回率)

    一.准确率,召回率 TP(True Positive):正确的正例,一个实例是正类并且也被判定成正类 FN(False Negative):错误的反例,漏报,本为正类但判定为假类 FP(False P ...

  7. 类的编写模板之简单Java类

    简单Java类是初学java时的一个重要的类模型,一般由属性和getter.setter方法组成,该类不涉及复杂的逻辑运算,仅仅是作为数据的储存,同时该类一般都有明确的实物类型.如:定义一个雇员的类, ...

  8. Vue3 企业级优雅实战 - 组件库框架 - 6 搭建example环境

    该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...

  9. Revit2021保姆级安装教程

    Revit2021 WIN10 64位安装步骤: 1.先使用"百度网盘客户端"下载Revit_2021软件安装包到电脑磁盘里,并解压缩,安装全程需连接网络,然后双击打开Revit_ ...

  10. 篇(16)-Asp.Net Core入门实战-权限管理之用户创建与关联角色(ViewModel再用与模型验证二)

    入门实战-权限管理之用户创建与关联角色(ViewModel再用与模型验证二) (1).在用户管理着模块中,相比较菜单功能的代码还是比较多的,设计到用户的创建,修改,角色变更和密码重置,同时都集中在列表 ...