这个专题主要从下面几个内容来展开介绍
- 1、什么是HDR?
- 2、算法实现
- 3、MTK Camera HAL3框架介绍
- 4、算法嵌入
1、什么是HDR?
问题
: 在我们平常使用的手机中,HDR
功能一般指的是Capture HDR(拍照HDR),而没有Preview HDR(预览HDR),为什么
?
HDR
是高动态范围图像(High-Dynamic Range)的简称,与普通的图像相比,拥有比较好的动态范围和图像细节,根据不同的曝光时间的LDR
(Low-Dynamic Range,低动态范围图像),利用每个曝光时间相对应最佳细节的LDR图像来合成最终的HDR图像,更好地反映真实环境中的视觉效果。
HDR
在很多不同的场景下都有实现,常见的比如电视、单反、手机等等,每个不同的应用场景,HDR
的一些实现标准都是不同。
简单点来理解,我们手机上的HDR
的图像一般是由摄像头采集三帧不同“亮度”(较低曝光度、常规曝光度和过度曝光)的图像数据进行合成而来的。真实的HDR处理算法需要将三帧图像合为一张,时间复杂度过高,会严重影响预览效果。 所以一般不会去实现预览HDR的效果。
2、算法实现
我们有没有办法即不影响预览实时性,又能对图像细节进行优化呢
? 为了不影响预览实时性,我们没办法对多帧图像进行合成,只能对当前帧进行处理。
在MTK的HAL3上,我们集成的3rd算法一般都是处理YUV格式的图像,具体的camera数据流后面再介绍,在这里大家只需要先知道我们的算法处理的是YUV格式的图像就可以。
Y:表示明亮度(Luminance、Luma)
U:表示色度(Chrominance)
V:表示浓度(Chroma)
YUV主要有三种采样的方式,分别是YUV444、YUV422、YUV420
YUV444:每一个Y对应一组UV分量
YUV422:每两个Y对应一组UV分量,常见的像素格式:YUYV、YVYU、UYVY、VYUY
YUV420:每四个Y对应一组UV分量,常见的像素格式:YV12、NV12、NV21
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
V V V V V V U V U V U V V U V U V U
U U U U U U U V U V U V V U V U V U
- YV12 - - NV12 - - NV21 -
2.1、灰度值直方图均衡化算法(HE)
2.1.1、基本概念介绍
这个算法里面涉及到几个基本的概念:
什么是灰度值?
什么是直方图?
什么是均衡化?为什么需要均衡化?
第一个问题: 摄像头采集到一帧图像数据,这一帧图像数据中每个点的颜色和亮度都是不同的,我们把白色和黑色之间按对数关系分成诺干等级,称为灰度等级
,范围一般是0~255,白色为255,黑色为0。这就是我们常说的灰度值
第二个问题: 对于一幅灰度图像,直方图可以反映这个图像中不同灰度级出现的统计情况。如下图,左边假设是一帧图像数据中的灰度值分布,对应右边就是这帧图像数据的灰度值直方图。
第三个问题: 摄像头采集到的每一帧图像,它的像素会在灰度等级0~255的范围内有不同分布。当摄像头处于一个过亮的环境时,摄像头采集到的图像会偏白
,也就是这帧图像中的像素灰度值,会集中在靠近“255”的地方;相反,当摄像头处于过暗的环境时(或者背光环境),采集到的图像中的像素灰度值,就会集中在比较靠近“0”的地方,整体效果就会偏暗。对于这两种情况来说,都会造成图像不够清晰。而均衡化的意思,就是将图像中的灰度分布,根据某种对应关系,将其分布得更加合理,进而提升图像细节效果。
直方图均衡化是一种简单有效的图像增强技术,通过改变图像的直方图来改变图像中各像素的灰度,主要用于增强动态范围偏小的图像的对比度。原始图像由于其灰度分布可能集中在较窄的区间,造成图像不够清晰。例如,过曝光图像的灰度级集中在高亮度范围内,而曝光不足将使图像灰度级集中在低亮度范围内。采用直方图均衡化,可以把原始图像的直方图变换为均匀分布(均衡)的形式,这样就增加了像素之间灰度值差别的动态范围,从而达到增强图像整体对比度的效果。换言之,直方图均衡化的基本原理是:对在图像中像素个数多的灰度值(即对画面起主要作用的灰度值)进行展宽,而对像素个数少的灰度值(即对画面不起主要作用的灰度值)进行归并,从而增大对比度,使图像清晰,达到增强的目的。
2.1.2、算法实现原理
HE算法的实现可以分成下面几个步骤:
- 遍历每一帧图像中的所有像素,记录每个灰度值出现的像素个数(形成灰度值直方图)
- 统计每个灰度值占总像素的百分比,也就是每个灰度值出现的概率(归一化)
- 建立一个映射表,对原图像的灰度值进行一一映射。映射公式 = 累积概率 * 255
(累积分布函数是概率密度函数的积分)
这里重点通过几个问题来进一步介绍HE算法的原理:
为什么选择累积分布函数?
累积分布函数如何实现灰度值的均衡化的?
第一个问题:为什么选择累积分布函数?
我们选择的映射函数应该满足两个要求:
单调递增:
只有使用单调递增映射函数,灰度值在映射后的大小顺序才不会发生改变,也就是灰度较小的映射后还是相对较小的,灰度较大的映射后还是相对较大的。区间不变:
前面已经介绍过,我们的灰度范围是0~255,无论如何映射,我们映射后的灰度值也应该落在这个范围内。
第二个问题:累积分布函数是如何实现灰度值的均衡化的?
以 [x1,x2] 区间为例,从均衡化前直方图可以看出,该区间内的灰度值数量较多,也就是概率密度较大,这样会导致该区间内累积分布函数快速增加,斜率也就相对较大,这样 [x1, x2]这个高概率密度区间就会被映射到一个宽的灰度范围内(将集中的灰度分配给其他灰度)。相反,在中低概率密度区,累积分布函数的增长相对比较缓慢,对应的斜率就会比较小,那么中低概率密度区间对应的灰度区间就会被映射到一个窄的灰度范围内。(将低概率的灰度级合并)
2.1.3、代码阅读
int ZK_HDR_Plugin::TvHdrReviewHandlerHE(const IImageBuffer *in, const unsigned char* src, unsigned char* dst)
{
MY_LOGD("Enter TvHdrReviewHandlerHE");
MSize in_size = in->getImgSize();
int height = in_size.h;
int width = in_size.w;
MY_LOGD("height = %d, width = %d\n", height, width);
const int hist_sz{
256 };
//hist用来记录每个灰度值出现的个数;lut是映射关系表,记录着映射关系
std::vector<int> hist(hist_sz, 0), hist_ps(hist_sz, 0);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
++hist[src[y * width + x]];
}
}
float sum = 0;
int total{
width * height };
int i{
0 };
for (hist_ps[i++] = 0; i < 256; i++) {
sum += hist[i];
hist_ps[i] = static_cast<int>(sum / total * 255 + 0.5f);
}
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
dst[y * width + x] = hist_ps[src[y * width + x]];
}
}
MY_LOGD("end TvHdrReviewHandlerHE");
return 0;
}
2.1.4、HE算法的缺点
如果一帧图像整体偏暗或者偏亮,那么直方图均衡化的方法就很实用。但是在某些情况下,由于HE算法本身的特点,会有一些不足之处。
- 直方图均衡化是一种全局处理,它对处理的数据不加选择,可能会增加背景干扰信息的对比度,并且降低有用信号的对比度。
- 直方图均衡化是对灰度值的一个展宽,均衡化后图像的灰度级会减少,某些细节会丢失。
2.2、自适应直方图均衡化算法(AHE)
上面说到了HE算法的两个缺点,一是对像素进行全局处理导致在某些环境下噪点也被同步放大;二是对全局像素进行灰度值展宽,会导致丢失过多的图像细节。自适应直方图均衡化算法针对HE算法的缺点,进行了一定程度上的优化。它是如何优化的?
(分块处理)
既然全局处理是HE的主要不足,那么我们对图像进行分块处理不就行了,这个就是AHE的原理了。它把一帧图像数据,分成好几块,比如分成8*8=64块,然后统计每一小块的灰度值直方图,然后再进行处理。
但是,这又衍生了两个问题
第一个问题:
因为把图像分成了许多独立的小块,而每一块都相当于是一个单独的个体进行算法处理,块与块之间没有进行过渡处理,这样就导致一个现象出现,那就是展示出来的图像看起来被分成了8*8的小块。如何解决这个问题?
(双线性插值)。
第二个问题:
本来一开始只需要统计一个灰度直方图出来,现在分成了8*8的小块,那么就需要统计出64个灰度直方图,这大大增加了算法的时间复杂度,实现出来预览就会出现卡顿的现象。如何解决?
(多线程)
什么是线性插值?
线性插值如何实现?
2.2.1、双线性插值法原理
要了解双线性插值法,需要先知道什么是线性插值。无论是线性插值还是双线性插值,它们的原理都是根据相邻值以及它们对于目标值来说所占权重,来计算出目标值。
比如:目前已知一条直线上两个点,(x0, y0)、(x1,y1),要得出在x0~x1区间中某一点x它在该直线上的值。这就是一个非常简单的二元一次方程
仔细看就是用x和x0,x1的距离作为一个权重,用于y0和y1的加权。这个就是线性插值的原理了。
在理想状态下,我们当然希望所有需要处理的点都是落在同一条二元一次方程上,这样我们直接做线程插值处理就可以了,当实际的应用中,比如我们现在所说的图像灰度处理,图像数据不可能都分布在同一条直线上,这样我们就需要用到双线性插值了。而双线性插值,顾名思义就是两个方向的线性插值加起来,分别在x轴和y轴都做一遍线性插值,就是双线性插值了。
2.2.2、双线性插值法在CLAHE算法的应用
我们使用的CLAHE算法,采用了三种不同的算法都一帧图像数据中的不同位置进行处理
边角
:直接使用普通的直方图均衡化算法边缘
:使用线性插值算法中心
:使用双线性插值算法
2.2.3、代码阅读
int ZK_HDR_Plugin::TvHdrReviewHandlerCLAHE(const IImageBuffer *in, const unsigned char* src, unsigned char* dst)
{
MY_LOGD("Enter TvHdrReviewHandlerCLAHE");
MSize in_size = in->getImgSize();
unsigned int height = in_size.h;
unsigned int width = in_size.w;
unsigned int block = 8; //将图像均匀分成等矩形大小,8行8列64个块是常用的选择
unsigned int width_block = width / block;
unsigned int height_block = height / block;
MY_LOGD("Riaan height = %d, width = %d", height, width);
/******************************** useless *************************************/
int **