自动白平衡算法

前置色彩基础

RGB色彩空间

RGB色彩空间是工业界的一种颜色标准,RGB是红绿蓝三原色的意思,R(Red)代表红色,G(Green)代表绿色,B(Blue)代表蓝色。

通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。

在计算机中一般用一个字节(8位)来表示一个R,一个字节表示G,一个字节表示B,总共占用24位,也就是说,R、G、B的取值都在0-255 (2 ^ 8-1),之间,总共可以表示16777216(2^24)中颜色。

任意一种色光F都可以用不同比例的R、G、B三色相加混合而成。

<!-- pr、pg、pb分别为三基色参与混合的系数 -->
F = pr [ R ] + pg [ G ] + pb [ B ]

当三原色比例都为0(最弱)时混合为黑色光
当三原色分量都为255(最强)时混合为白色光
调整pr、pg、pb三个系数的值,可以混合出介于黑色光和白色光之间的各种各样的色光

YUV颜色空间

YUV也是一种颜色空间,基于YUV的颜色编码是流媒体的常用编码方式。Y表示亮度,UV确认其彩度。

采用YUV色彩空间重要的是它的亮度信号Y和色度信号U、V是分离的。如果只有Y信号分量而没有U、V分量,那么这样表示的图像就是黑白灰度图像。彩色电视采用YUV空间正是为了用亮度信号Y解决彩色电视机与黑白电视机的兼容问题,使黑白电视机也能接收彩色电视信号。

YCbCr

YCbCr是YUV经过缩放和偏移翻版,只是在表示方法上不同而已。其中Y与YUV中的Y含义一致,是指亮度分量,Cb,Cr同样都指色彩,C代表颜色,b代表蓝色,r代表红色,Cb指蓝色色度分量,而Cr指红色色度分量。

YCbCr主要用于数字图像领域,YUV主要用于模拟信号领域,MPEG、DVD、摄像机中常说的YUV其实是YCbCr,二者转换为RGBA的转换矩阵是不同的。

在现代彩色电视系统中,通常采用三管彩色摄像机或彩色CCD摄像机进行摄像,然后把摄得的彩色图像信号经分色、分别放大校正后得到RGB,再经过矩阵变换电路得到亮度信号Y和两个色差信号RY(即U)、BY(即V),最后发送端将亮度和色差三个信号分别进行编码,用同一信道发送出去。

注意:Y、Cb、Cr信号是分开储存的,所以DVD播放机要连三条线(DVD使用MPEG2格式)

YUV压缩

因为人类视网膜上的视网膜杆细胞要多于视网膜锥细胞,视网膜杆细胞的作用就是识别亮度,而视网膜锥细胞的作用就是识别色度。所以,眼睛对于亮和暗的分辨要比对颜色的分辨精细一些。

因此在视频存储中,没有必要存储全部颜色信号,存储了眼睛分辨不出来,因此没有必要浪费存储空间,于是有了YUV压缩。

常见的YUV压缩格式有4:4:4,4:2:2,色度信号分辨率最高的格式是4:4:4,也就是说,每4点Y采样,就有相对应的4点Cb和4点Cr。在这种格式中,色度信号的分辨率和亮度信号的分辨率是相同的。这种格式主要应用在视频处理设备内部,避免画面质量在处理过程中降低。

RGB与YUV转换

YUV与RGB可以相互转换,公式如下:

RGB转YUV:

Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B

YUV转RGB:

R = Y + 1.14V
G = Y - 0.39U - 0.58V
B = Y + 2.03U

白平衡算法

灰度世界算法(Gray World)

灰度世界算法以灰度世界假设为基础,该假设认为:对于一幅有着大量色彩变化的图像,R,G,B三个分量的平均值趋于同一灰度值Gray。从物理意义上讲,灰色世界法假设自然界景物对于光线的平均反射的均值在总体上是个定值,这个定值近似地为“灰色”,从而消除环境光的影响获得原始场景图像。

  1. 计算Gray值,一般都两种方法,A:使用固定值对于8位的图像(0~255)通常取128作为灰度值。B:分别计算三通道的平均值avgR,avgG,avgB,则:Gray=(avgR+avgG+avgB)/3
  2. 计算增益系数,kr=Gray/avgR , kg=Gray/avgG , kb=Gray/avgB
  3. 利用增益系数,重新计算每个像素值,构成新的图片RGB(NR=R * kr,NG=G * kg,NB = B * kb)

优缺点:这种算法简单快速,但是当图像场景颜色并不丰富时,尤其出现大块单色物体时,也就是色彩够不多样时该算法常会失效。

def gray_world(src_img_path, dest_img_path):
    """
    灰度世界算法
    """
    src_img = Image.open(src_img_path)
    # 图形转numpy数组
    image_arr = np.array(src_img).astype(np.uint32)
    # 分离RGB
    image_arr = image_arr.transpose(2, 0, 1)

    avg_red = np.average(image_arr[0])
    avg_green = np.average(image_arr[1])
    avg_blue = np.average(image_arr[2])

    gray = (avg_red + avg_green + avg_blue) / 3

    kred = gray / avg_red
    kgreen = gray / avg_green
    kblue = gray / avg_blue

    image_arr[0] = np.minimum(image_arr[0] * kred, 255)
    image_arr[1] = np.minimum(image_arr[1] * kgreen, 255)
    image_arr[2] = np.minimum(image_arr[2] * kblue, 255)

    # 重新合并RGB
    image_arr = image_arr.transpose(1, 2, 0)
    # numpy数组转图像
    img = Image.fromarray(image_arr.astype('uint8')).convert('RGB')
    img.save(dest_img_path)

可调节色温灰度世界算法

def get_avg(avg_red, avg_green, avg_blue, type):
    if type is None or type == 0:
        return avg_red, avg_green, avg_blue
    # 阴天 7500k
    elif type == 1:
        return avg_red * 1.953125, avg_green * 1.0390625, avg_blue
    # 日光 6500k
    elif type == 2:
        return avg_red * 1.2734375, avg_green, avg_blue * 1.0625
    # 白热光 5000k
    elif type == 3:
        return avg_red * 1.2890625, avg_green, avg_blue * 1.0625
    # 日光灯 4400k
    elif type == 4:
        return avg_red * 1.1875, avg_green, avg_blue * 1.3125
    # 钨丝灯 2800k
    elif type == 4:
        return avg_red, avg_green * 1.0078125, avg_blue * 1.28125
    else:
        return avg_red, avg_green, avg_blue


def gray_world_two(src_img_path, dest_img_path, type):
    """
    灰度世界算法
    """
    src_img = Image.open(src_img_path)
    # 图形转numpy数组
    image_arr = np.array(src_img).astype(np.uint32)
    # 分离RGB
    image_arr = image_arr.transpose(2, 0, 1)

    avg_red = np.average(image_arr[0])
    avg_green = np.average(image_arr[1])
    avg_blue = np.average(image_arr[2])

    avg_red, avg_green, avg_blue = get_avg(avg_red, avg_green, avg_blue, type)

    gray = (avg_red + avg_green + avg_blue) / 3

    kred = gray / avg_red
    kgreen = gray / avg_green
    kblue = gray / avg_blue

    image_arr[0] = np.minimum(image_arr[0] * kred, 255)
    image_arr[1] = np.minimum(image_arr[1] * kgreen, 255)
    image_arr[2] = np.minimum(image_arr[2] * kblue, 255)

    # 重新合并RGB
    image_arr = image_arr.transpose(1, 2, 0)
    # numpy数组转图像
    img = Image.fromarray(image_arr.astype('uint8')).convert('RGB')
    img.save(dest_img_path)

完美反射算法(Perfect Reflector)

完美全反射理论假设图像上最亮点就是白点(R+G+B的最大值),并以此白点为参考对图像进行自动白平衡。

  1. 遍历原始图像,统计RGB三通道之和的直方图
  2. 遍历原始图像,找到RGB三通道各自的最大值Bmax、Gmax、Rmax
  3. 设定比例r,对RGB之和的直方图进行倒叙遍历,找到使白点像素个数超过总像素个数比例的阈值T
  4. 遍历原始图像,计算RGB之和大于T的像素,各个通道取平均,得到Bavg、Gavg、Ravg
  5. 遍历原始图像,分别计算RGB三通道的调整值Aout=A / Aavg * Amax

注意:计算可能溢出

优缺点:比灰度世界算法好点,但是依赖top值选取而且对亮度最大区域不是白色的图像效果不佳。

def perfect_reflector(src_img_path, dest_img_path, ratio):
    """
    完美反射算法
    """
    src_img = Image.open(src_img_path)
    image_arr = np.array(src_img).astype(np.uint8)
    w, h, v = image_arr.shape
    hist_rgb = {}
    for i in range(w):
        for j in range(h):
            rgb_sum = int(image_arr[i, j, 0]) + int(image_arr[i, j, 1]) + int(image_arr[i, j, 2])
            hist_rgb.setdefault(rgb_sum, 0)
            hist_rgb[rgb_sum] += 1

    total_sum = 0
    for i in range(767, 1, -1):
        try:
            total_sum += hist_rgb[i]
        except KeyError:
            continue
        if total_sum > w * h * ratio / 100.0:
            threshold = i
            break

    count = 0
    avg_red = 0;
    avg_green = 0;
    avg_blue = 0
    for i in range(w):
        for j in range(h):
            t = int(image_arr[i, j, 0]) + int(image_arr[i, j, 1]) + int(image_arr[i, j, 2])
            if t > threshold:
                avg_red += int(image_arr[i, j, 0])
                avg_green += int(image_arr[i, j, 1])
                avg_blue += int(image_arr[i, j, 2])
                count += 1
    avg_red /= count
    avg_green /= count
    avg_blue /= count

    maxvalue = np.max(image_arr)
    for i in range(w):
        for j in range(h):
            red = int(image_arr[i, j, 0]) * maxvalue / int(avg_red)
            green = int(image_arr[i, j, 1]) * maxvalue / int(avg_green)
            blue = int(image_arr[i, j, 2]) * maxvalue / int(avg_blue)
            if red > 255:
                red = 255
            elif red < 0:
                red = 0
            if green > 255:
                green = 255
            elif green < 0:
                green = 0
            if blue > 255:
                blue = 255
            elif blue < 0:
                blue = 0
            image_arr[i, j, 0] = red
            image_arr[i, j, 1] = green
            image_arr[i, j, 2] = blue

    img = Image.fromarray(image_arr.astype('uint8')).convert('RGB')
    img.save(dest_img_path)

动态阈值白平衡算法

动态阈值白平衡算法比较复杂,但是原理还是一样,用白色来作为调整的基色,检查白色点,计算增益参数,调整愿图像。

动态阈值白平衡算法采用一个动态的阀值来检测白色点。下面简单介绍一下算法过程:

  1. 把图像从RGB空间转换到YCrCb空间
  2. 把图像分成宽高比为4:3的块
  3. 对每个块,分别计算Cr,Cb的平均值Mr,Mb
  4. 对每个块,根据Mr,Mb,用下面公式分别计算Cr,Cb的方差Dr,Db
  5. 判定每个块的近白区域,设一个“参考白色点”的亮度矩阵RL。若符合判别式,则作为“参考白色点”,并把该点的亮度值赋给RL(i,j);若不符合,则该点的RL(i,j)值为0。
  6. 选取参考“参考白色点”中最大的10%的亮度(Y分量)值,并选取其中的最小值Lu_min
  7. 调整RL,若RL(i,j) < Lu_min, RL(i,j)=0; 否则,RL(i,j)=1
  8. 分别把R,G,B与RL相乘,得到R2,G2,B2。分别计算R2,G2,B2的平均值,Rav,Gav,Bav
  9. 得到调整增益:Ymax=double(max(max(Y)))/5;Rgain=Ymax/Rav;Ggain=Ymax/Gav;Bgain=Ymax/Bav
  10. 调整原图像:Ro= RRgain; Go= GGgain; Bo= B*Bgain

下面是一个Java实现的动态阈值算法:

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class WhiteBalance {

    public static void dynamicThreshold(String srcImgFilePath,String destImgFilePath) throws IOException {
        File srcFile = new File(srcImgFilePath);
        BufferedImage img = ImageIO.read(srcFile);
        int pixelsize = img.getWidth() * img.getHeight();
        double[][][] YCbCr = new double[img.getWidth()][img.getHeight()][3];
        double mr = 0, mb = 0, yMax = 0;
        for (int i = 0; i < img.getWidth(); i++) {
            for (int j = 0; j < img.getHeight(); j++) {
                YCbCr[i][j] = rgbToYCbCr(img.getRGB(i, j));
                mr += YCbCr[i][j][2];
                mb += YCbCr[i][j][1];
                yMax = Math.max(yMax, YCbCr[i][j][0]);
            }
        }

        mr /= pixelsize;
        mb /= pixelsize;

        double dr = 0, db = 0;
        for (int i = 0; i < YCbCr.length; i++) {
            for (int j = 0; j < YCbCr[i].length; j++) {
                db += Math.abs(YCbCr[i][j][1] - mb);
                dr += Math.abs(YCbCr[i][j][2] - mr);
            }
        }
        dr /= pixelsize;
        db /= pixelsize;

        double[][] Y = new double[img.getWidth()][img.getHeight()];
        double[] yHistogram = new double[256];
        double ySum = 0;
        for (int i = 0; i < Y.length; i++) {
            for (int j = 0; j < Y[i].length; j++) {
                int value = (Math.abs(YCbCr[i][j][1] - (mb + db * Math.signum(mb))) < 1.5 * db) & (Math.abs(YCbCr[i][j][2]) - (1.5 * mr + dr * Math.signum(mr))) < 1.5 * dr ? 1 : 0;
                if (value <= 0)
                    continue;
                double y = YCbCr[i][j][0];
                Y[i][j] = y;
                yHistogram[(int) Y[i][j]]++;
                ySum++;
            }
        }

        double yHistogramsum = 0;
        double yMmin = 0;
        for (int i = yHistogram.length - 1; i >= 0; i--) {
            yHistogramsum += yHistogram[i];
            if (yHistogramsum > 0.1 * ySum) {
                yMmin = i;
                break;
            }
        }

        double redAvg = 0, greenAvg = 0, blueAvg = 0;
        double averSum = 0;
        for (int i = 0; i < Y.length; i++) {
            for (int j = 0; j < Y[i].length; j++) {
                if (Y[i][j] > yMmin) {
                    int color = img.getRGB(i, j);
                    int r = (color >> 16) & 0xFF;
                    int g = (color >> 8) & 0xFF;
                    int b = color & 0xFF;
                    redAvg += r;
                    greenAvg += g;
                    blueAvg += b;
                    averSum++;
                }
            }
        }
        redAvg /= averSum;
        greenAvg /= averSum;
        blueAvg /= averSum;

        double redGain = yMax / redAvg;
        double greenGain = yMax / greenAvg;
        double blueGain = yMax / blueAvg;
        for (int i = 0; i < img.getWidth(); i++) {
            for (int j = 0; j < img.getHeight(); j++) {
                Color color = new Color(img.getRGB(i, j));
                int r = ensureColor((int) Math.floor(color.getRed() * redGain));
                int g = ensureColor((int) Math.floor(color.getGreen() * greenGain));
                int b = ensureColor((int) Math.floor(color.getBlue() * blueGain));
                img.setRGB(i, j, new Color(r, g, b).getRGB());
            }
        }
        File destFile = new File(destImgFilePath);
        int index = destImgFilePath.lastIndexOf(".");
        String format;
        if(index == -1){
            format = "png";
        }else {
            format = destImgFilePath.substring(index + 1);
        }
        ImageIO.write(img, format, destFile);
    }

    /**
     * RGB转换为YCbCr
     * @param color
     * @return
     */
    private static double[] rgbToYCbCr(int color) {
        int r = (color >> 16) & 0xFF;
        int g = (color >> 8) & 0xFF;
        int b = color & 0xFF;

        double Y = 16 + (65.481 * r / 255 + 128.553 * g / 255 + 24.966 * b / 255);
        double Cb = 128 + (-37.797 * r / 255 - 74.203 * g / 255 + 112 * b / 255);
        double Cr = 128 + (112 * r / 255 - 93.786 * g / 255 - 18.214 * b / 255);
        return new double[] { Y, Cb, Cr };
    }

    /**
     * 范围检测
     * @param color
     * @return
     */
    private static int ensureColor(double color) {
        if (color < 0)
            return 0;
        if (color > 255)
            return 255;
        return (int) color;
    }

    public static void main(String[] args) throws IOException {
        dynamicThreshold("G:\\tmp\\2d.jpg","G:\\tmp\\dynamic_threshold.jpg");
    }
}

不同算法效果对比

原图
动态阈值算法
灰度世界
灰度世界阴天
灰度世界日光
灰度世界白热光
灰度世界日光灯
灰度世界钨丝灯
完美反射算法
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容