前置色彩基础
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。从物理意义上讲,灰色世界法假设自然界景物对于光线的平均反射的均值在总体上是个定值,这个定值近似地为“灰色”,从而消除环境光的影响获得原始场景图像。
- 计算Gray值,一般都两种方法,A:使用固定值对于8位的图像(0~255)通常取128作为灰度值。B:分别计算三通道的平均值avgR,avgG,avgB,则:Gray=(avgR+avgG+avgB)/3
- 计算增益系数,kr=Gray/avgR , kg=Gray/avgG , kb=Gray/avgB
- 利用增益系数,重新计算每个像素值,构成新的图片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的最大值),并以此白点为参考对图像进行自动白平衡。
- 遍历原始图像,统计RGB三通道之和的直方图
- 遍历原始图像,找到RGB三通道各自的最大值Bmax、Gmax、Rmax
- 设定比例r,对RGB之和的直方图进行倒叙遍历,找到使白点像素个数超过总像素个数比例的阈值T
- 遍历原始图像,计算RGB之和大于T的像素,各个通道取平均,得到Bavg、Gavg、Ravg
- 遍历原始图像,分别计算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)
动态阈值白平衡算法
动态阈值白平衡算法比较复杂,但是原理还是一样,用白色来作为调整的基色,检查白色点,计算增益参数,调整愿图像。
动态阈值白平衡算法采用一个动态的阀值来检测白色点。下面简单介绍一下算法过程:
- 把图像从RGB空间转换到YCrCb空间
- 把图像分成宽高比为4:3的块
- 对每个块,分别计算Cr,Cb的平均值Mr,Mb
- 对每个块,根据Mr,Mb,用下面公式分别计算Cr,Cb的方差Dr,Db
- 判定每个块的近白区域,设一个“参考白色点”的亮度矩阵RL。若符合判别式,则作为“参考白色点”,并把该点的亮度值赋给RL(i,j);若不符合,则该点的RL(i,j)值为0。
- 选取参考“参考白色点”中最大的10%的亮度(Y分量)值,并选取其中的最小值Lu_min
- 调整RL,若RL(i,j) < Lu_min, RL(i,j)=0; 否则,RL(i,j)=1
- 分别把R,G,B与RL相乘,得到R2,G2,B2。分别计算R2,G2,B2的平均值,Rav,Gav,Bav
- 得到调整增益:Ymax=double(max(max(Y)))/5;Rgain=Ymax/Rav;Ggain=Ymax/Gav;Bgain=Ymax/Bav
- 调整原图像: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");
}
}