图像基本属性调整

色彩模型

RGB色彩模型。

RGB是依据人眼识别的颜色定义出的空间,可表示大部分颜色。是图像处理中最基本、最常用、面向硬件的颜色空间,是一种光混合的体系。

可以看到RGB颜色模式用三维空间中的一个点表示一种颜色,每个点有三个分量,分别表示红、绿、蓝的亮度值,亮度值限定为【0,1】。在RGB模型的立方体中,原点对应的颜色为黑色,它的三个分量值都为0;距离原点最远的顶点对应的颜色为白色,它的三个分量值都为1。从黑色到白色的灰度值分布在这两个点的连线上,该虚线称为灰度线;立方体的其余各点对应不同的颜色,即三原色红、绿、蓝及其混合色黄、品红、青色。

HSI色彩模型

HSI色彩空间是从人的视觉系统出发,用色调(Hue)、饱和度(Saturation或Chroma)和亮度 (Intensity或Brightness)来描述色彩。

  • H: 表示颜色的相位角。红、绿、蓝分别相隔120度;互补色分别相差180度,即颜色的类别。
  • S: 表示成所选颜色的纯度和该颜色最大的纯度之间的比率,范围:[0, 1],即颜色的深浅程度。
  • I: 表示色彩的明亮程度,范围:[0, 1],人眼对亮度很敏感!

可以看到HSI色彩空间和RGB色彩空间只是同一物理量的不同表示法,因而它们之间存在着转换关系:HSI颜色模式中的色调使用颜色类别表示,饱和度与颜色的白光光亮亮度刚好成反比,代表灰色与色调的比例,亮度是颜色的相对明暗程度。

CMYK模型,用于印刷品依靠反光的色彩模式

CMYK是一种依靠反光的色彩模式,我们是怎样阅读报纸的内容呢?是由阳光或灯光照射到报纸上,再反射到我们的眼中,才看到内容。它需要有外界光源,如果你在黑暗房间内是无法阅读报纸的。只要在屏幕上显示的图像,就是RGB模式表现的。只要是在印刷品上看到的图像,就是CMYK模式表现的。大多数在纸上沉积彩色颜料的设备,如彩色打印机和复印机,要求输入CMY数据,在内部进行RGB到CMY的转换。

青色Cyan、品红色Magenta、黄色Yellow是光的二次色,是颜料的颜色。而K取的是black最后一个字母,之所以不取首字母,是为了避免与蓝色(Blue)混淆。当红绿蓝三原色被混合时,会产生白色,当混合青色、品红色、黄色三原色时会产生黑色。从理论上来说,只需要CMY三种油墨就足够了,但是由于目前制造工艺还不能造出高纯度的油墨,CMY相加的结果实际是一种暗红色。

RGB 图像灰度化

RGB转换为灰度图有如下几种方法:

  1. 分量法:任选一通道作为 gray
ini 复制代码
gray = R or gray=G or gray=B
  1. 最大值法:RGB 中最大值作为 gray
ini 复制代码
gray=max(R,G,B)
  1. 平均值法:RGB 中平均值作为 gray
ini 复制代码
gray=mean(R,G,B)
  1. 加权平均值法:RGB 中加权平均值作为 gray, 因为人眼对红绿蓝三色的敏感程度不同,所以计算灰度的时候要加权平均。这个系数主要是根据人眼对R,G,B三原色的敏感性不同而导出的系数.
ini 复制代码
gray = 0.299 * R + 0.578 * G + 0.114 * B
//该权重是RGB转YUV的BT709明亮度转换公式,是基于人眼感知的图像灰度处理公式
gray = 0.2125 * R + 0.7154 * G + 0.0721 * B
ini 复制代码
static const GLchar * const kGrayscaleFragmentShader = STRINGIZE
(
 precision highp float;
 
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);
 
 void main()
 {
     lowp vec4 textureColor = texture2D(texture, v_texcoord);
     float luminance = dot(textureColor.rgb, W);
     
     gl_FragColor = vec4(vec3(luminance), textureColor.a);
 }
 );

色相

Hue即色相,就是我们平时所说的红、绿,蓝。如果你分的更细的话可能还会有洋红、草绿等等;在HSV模型中,用度数来描述色相,其中红色对应0度,绿色对应120度,蓝色对应240度。

如何调整色相:通过将 RGB 空间转换到 YIQ/YCbCr 空间,调整其中的 chroma(I&Q, or Cb&Cr) 分量。

  1. 将 RGB 转换到 YIQ
  2. 计算色相值,单位弧度: hue = atan(Q, I);
  3. 计算长边 chroma = sqart(I * I + Q * Q);
  4. 旋转色相,调整 hue 弧度值
  5. 旋转后的 hue 弧度值,利用正玄/余玄,计算 I/Q; Q = sin(hue) * chroma, I = cos(hue) * chroma
  6. 将 YIQ 转换到 RGB
ini 复制代码
static const GLchar *const kHueFragmentShader = STRINGIZE
(
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform mediump float hueAdjust;
 
 const highp vec4 kRGBToYPrime = vec4(0.299, 0.587, 0.114, 0.0);
 const highp vec4 kRGBToI     = vec4(0.595716, -0.274453, -0.321263, 0.0);
 const highp vec4 kRGBToQ     = vec4(0.211456, -0.522591, 0.31135, 0.0);
 
 const highp vec4 kYIQToR = vec4(1.0, 0.9563, 0.6210, 0.0);
 const highp vec4 kYIQToG = vec4(1.0, -0.2721, -0.6474, 0.0);
 const highp vec4 kYIQToB = vec4(1.0, -1.1070, 1.7046, 0.0);
 
 void main ()
 {
     // Sample the input pixel
     highp vec4 color = texture2D(texture, v_texcoord);
     
     // Convert to YIQ
     highp float YPrime  = dot(color, kRGBToYPrime);
     highp float I      = dot(color, kRGBToI);
     highp float Q      = dot(color, kRGBToQ);
     
     // Calculate the hue and chroma
     highp float hue     = atan(Q, I);
     highp float chroma  = sqrt(I * I + Q * Q);
     
     // Make the user's adjustments
     hue += (-hueAdjust); //why negative rotation?
     
     // Convert back to YIQ
     Q = chroma * sin(hue);
     I = chroma * cos(hue);
     
     // Convert back to RGB
     highp vec4 yIQ = vec4(YPrime, I, Q, 0.0);
     color.r = dot(yIQ, kYIQToR);
     color.g = dot(yIQ, kYIQToG);
     color.b = dot(yIQ, kYIQToB);
     
     // Save the result
     gl_FragColor = color;
 }
 );

亮度

亮度表示颜色的明亮程度,明度越高,颜色越亮。

对于灰度图像来说,每个像素点只有1个分量,且在0~255之间,0表示黑色,最暗,1表示白色,最亮。

对彩色图像来说,每个像素点有3个分量(RGB),每个分量的值在0~255之间,RGB的各个值越小,亮度越小;RGB的各个值越大,亮度越大,所以想要控制亮度,我们只要控制RGB值大小即可

如何调整亮度:通过增加和减少像素分量的值,亮度调整参数 brightness 取值范围 -1.0 to 1.0,

ini 复制代码
static const GLchar *const kBrightnessFragmentShader = STRINGIZE
( 
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform float brightness;
 
 void main()
 {
     vec4 textureColor = texture2D(texture, v_texcoord);
     gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
 }
 );

饱和度

图像的饱和度是指色彩的鲜艳程度,也称色彩的纯度。饱和度取决于该色中含色成分和消色成分(灰色)的比例。含色成分越大,饱和度越大;消色成分越大,饱和度越小。纯的颜色都是高度饱和的,如鲜红,鲜绿。混杂上白色,灰色或其他色调的颜色,是不饱和的颜色,如绛紫,粉红,黄褐等。完全不饱和的颜色根本没有色调,如黑白之间的各种灰色

往一种颜色中添加大量的黑白灰色,当该颜色中的黑白灰色的量远大于原颜色的量时,可以认为该颜色的灰度值就是该颜色饱和度为0时的RGB值。因此,当一个画面饱和度为0时,得到的应该是该画面对应的灰度图。

如何调整饱和度:饱和度调整参数 saturation 取值范围 0.0 (fully desaturated) to 2.0 (max saturation), with 1.0 as the normal level

  1. 计算灰度值:0.2125 * R + 0.7154 * G + 0.0721 * B
  2. 使用 mix 或者 lerp 函数,将原色与灰度值按比例混合
markdown 复制代码
`mix`是一个特殊线性插值函数,两个参数值基于第三个参数插值`genType mix(genType x,genType y,float a)`,即`(x*(1-a)+y*a)`。简单理解就是`a`的值决定了`x`和`y`的强弱关系。`a`取值范围在`[0,1]`之间,`a`值越大,结果值中`y`占比会越大;`a`值越小,结果值中`y`占比会越小;
`lerp`函数的用法:`lerp(x, y, a): 用于插值,公式定义 x*(1-a) + y*a .af为百分数(取值范围[0,1])
ini 复制代码
static const GLchar *const kSaturationFragmentShader = STRINGIZE
(
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform lowp float saturation;
 
 // Values from "Graphics Shaders: Theory and Practice" by Bailey and Cunningham
 // 该权重是RGB转YUV的BT709明亮度转换公式,是基于人眼感知的图像灰度处理公式
 const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721);
 
 void main()
 {
     lowp vec4 textureColor = texture2D(texture, v_texcoord);
     lowp float luminance = dot(textureColor.rgb, luminanceWeighting);
     lowp vec3 grayScaleColor = vec3(luminance);
     // mix(x, y, a) = x * (1 - a) + y * a
     gl_FragColor = vec4(mix(grayScaleColor, textureColor.rgb, saturation), textureColor.w);
 }
 );

对比度

图像的对比度是指图像中明暗区域最亮的白和最暗的黑之间不同亮度层级的测量,即图像灰度反差的大小,通俗一点来说就是最大亮度与最小亮度之比,对比度越大,图像越清晰,色彩也越鲜艳;对比度越小,画面越显示的灰蒙蒙。

如何调整对比度:纹理RGB减去一半色值后和contrast值相乘,最后再加上被减去的一半色,contrast的调节范围在[0,2]之间。当contrast为1.0时是原图对比度,contrast越大,画面最亮和最暗像素之间差值越大;contrast越小,画面最亮和最暗像素之间差值越小;

ini 复制代码
static const GLchar * const kContrastFragmentShader = STRINGIZE
(
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform float contrast;
 
 void main()
 {
     vec4 textureColor = texture2D(texture, v_texcoord);
     gl_FragColor = vec4(((textureColor.rgb - vec3(0.5)) * contrast + vec3(0.5)), textureColor.w);
 }
 );

曝光度

曝光度与亮度相似,调整 RGB 值大小,越大曝光度越高。但数值变化方式不一样,亮度是全方位的线性增加色值,而曝光度是基于原色值的指数型叠加(红的会更红,绿的会更绿,蓝的会更蓝,白光的会更光)

ini 复制代码
static const GLchar *const kExposureFragmentShader = STRINGIZE
(
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 uniform highp float exposure;
 
 void main()
 {
     highp vec4 textureColor = texture2D(texture, v_texcoord);
     
     gl_FragColor = vec4(textureColor.rgb * pow(2.0, exposure), textureColor.w);
 }
 );

色阶

色阶表示图像亮度强弱的指数标准,也就是我们说的色彩指数,在数字图像处理教程中,指的是灰度分辨率(又称为灰度级分辨率或者幅度分辨率.)。图像的色彩丰满度和精细度是由色阶决定的。色阶是指亮度,和颜色无关,但最亮的只有白色,最暗的只有黑色。

伽马校正

下面给出物理线性亮度变化和人类察觉的线性亮度变化:

上一行的人类感知线性亮度更像是我们眼睛看到的亮度变化。然而当我们考虑物理亮度数值(光子层面)时,下一行反而是正确的亮度变化。所以真实的物理亮度和我们看见的视觉亮度是不一样的。

物理上的亮度是根据单位面积光子的数量定义的,1-100的物理亮度就是严格的线性递增;而人眼并不是这样,人眼通过"对比"来感受亮度的变化,0-1的变化能被人很明显地感受到,但是100-101的变化就不行了,所以说人眼感知的亮度和物理亮度必然是不对等的(根据韦伯定律)

如果我们直接将图片RGB以物理亮度保存,那么大概物理亮度的0.2就代表了心理上0.5的亮度,这样一来暗部只能占0-0.2这一小范围,如果图片按照每通道8bit方式存储,那么暗部只能占到大概50个阶,这样会导致偏暗的图像丢失很多细节。为了有效利用有限的空间,存储心理上的亮度值是更好的做法,这样能让亮部暗部各占一半;

物理亮度和视觉亮度的关系大概可以拟合成幂函数关系,我们将幂记为编码Gamma。幂函数的幂一般在1.8-2.5之间,

为了有效利用空间,我们存储心理上的亮度值,但是显示器可不是人,它只认识严格的物理量,所以我们将图片交由显示器显示时,需要再次将亮度值转换为物理亮度,转换用的幂值记为解码Gamma

最早的CRT显示器的输入电压和显示亮度的关系也不是线性的,他们的关系大概像幂函数,幂大概在2.0-3.0之间。

可以发现,两个函数曲线是近似对称的,那就可以约定一个相同的幂值来统合编码和解码Gamma,这个值就是常见的2.2,存储编码时使用1/2.2作为幂,然后直接输入给显示器(显示器内部以2.2为幂解码),就可以达到比较理想的效果。虽然现代液晶显示器已经没有了早期CRT显示器的问题,但是为了兼容,一般也会设有2.2左右的默认Gamma值。

伽马校正 :我们日常中使用的大部分图片(比如sRGB标准)都是经过Gamma编码的,存储的是经过非线性映射的RGB值,那么就存在一个问题,如果我们将这种图片用作纹理贴图,然后需要对他们进行一些比如光照计算、模糊等处理的时候,如果我们直接用存储的RGB值来计算,就会出现错误的效果,因为做这些计算时我们依据的是真实物理量,但是存在图片中的RGB并不是

所以我们需要先将图片中的经过编码后的RGB值先解码真实物理值,然后根据需要对它们做计算处理,一切完成以后再将结果数据重新编码。

ini 复制代码
static const GLchar *const kGammaFragmentShader = STRINGIZE
(
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform lowp float gamma;
 
 void main()
 {
    lowp vec4 textureColor = texture2D(texture, v_texcoord);
    
    gl_FragColor = vec4(pow(textureColor.rgb, vec3(1/2.2)), textureColor.w);
}
 );

反色

关于图片反色的定义还是很容易理解的,我们已经知道在GL中颜色是用r,g,b,a表示的,r,g,b,a 的范围是0.0f~1.0f,若染色color = vec4(r,g,b,a),则反色的颜色

所以反色的fragment shader 自然而然就是下面了

ini 复制代码
static const GLchar *const kColorInvertFragmentShader = STRINGIZE
(
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 void main()
 {
     vec4 textureColor = texture2D(texture, v_texcoord);
     gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);
 }
 );

颜色矩阵

可通过用 4×4 矩阵乘以这些颜色矢量将线性变换(旋转和缩放等)应用到颜色矢量中。但是,您不能使用 4×4 矩阵进行平移(非线性)。如果在每个颜色矢量中再添加一个虚拟的第 5 坐标(例如,数字 1),则可使用 5×5 矩阵应用任何组合形式的线性变换和平移。由线性变换组成的后跟平移的变换称为仿射变换。

ColorMatrix是怎样实现颜色的缩放、旋转、剪切及平移的?靠这些功能能实现图像的哪些效果?或者说,某种效果能用ColorMatrix实现吗?

ColorMatrix 矩阵

R G B A V
m11 m12 m13 m14 m15
m21 m22 m23 m24 m25
m31 m32 m33 m34 m35
m41 m42 m43 m44 m45
m51 m52 m53 m54 m55

ColorMatrix 单位矩阵

R G B A V
1.0 0.0 0.0 0.0 0.0
0.0 1.0 0.0 0.0 0.0
0.0 0.0 1.0 0.0 0.0
0.0 0.0 0.0 1.0 0.0
0.0 0.0 0.0 0.0 1.0

对于颜色的每个分量R、G、B、A来说,运用ColorMatrix后所得到的实际值r、g、b、a,用公式表示为:

ini 复制代码
r = R * m11 + G * m21 + B * m31 + A * m41 + m51 * 255;
g = R * m12 + G * m22 + B * m32 + A * m42 + m52 * 255;
b = R * m13 + G * m23 + B * m33 + A * m43 + m53 * 255;
a = R * m14 + G * m24 + B * m34 + A * m44 + m54 * 255;
ini 复制代码
static const GLchar *const kColorMatrixFragmentShader = STRINGIZE
(
 precision highp float;
 varying highp vec2 v_texcoord;
 uniform sampler2D texture;
 
 uniform lowp mat4 colorMatrix;
 uniform lowp float intensity;
 
 void main()
 {
    lowp vec4 textureColor = texture2D(texture, v_texcoord);
    lowp vec4 outputColor = textureColor * colorMatrix;
    gl_FragColor = outputColor;
    
}
 );

颜色缩放

颜色缩放:颜色缩放很简单,就是按照给定的比例值,在图像像素现有A、R、G、B各分量数值基础上计算出新的分量值。这个比例值就是ColorMatrix主对角线除m55外的其它4个值。比如某像素的RGBA值现在分别为255、128、64和255,而主对角线m11 - m44的值分别为0.8、0.5、-1及0.5,那么该像素新的rgba值应该是

ini 复制代码
r = R * m11 = 255 * 0.8 = 204;
g = G * m22 = 128 * 0.5 = 64;
b = B * m33 = 64 * -1 = 192; // 64 * -1应该等于-64。没错,64用32位数表示为0xFFFFFFC0,无符号字节饱和取整,取最后8位0xC0,等于192
a = A * m44 = 255 * 0.5 = 128;

颜色剪切

一般说来,图像像素R、G、B各分量按照与另一种颜色分量成比例的量来增加或减少颜色分量就是剪切。其实这种表述并不完全,像素的A分量也是参与其中的

以红色分量R举例,如果要按绿色分量G进行剪切,那么m21就是剪切比例值,m21 * G就得到了G对R的剪切量。同理,m31 * B、m41 * A可分别得到B和A对R的剪切量,将这些剪切量加起来,就是R总的剪切量。用公式表示为:

ini 复制代码
r = G * m21 + B * m31 + A * m41;
g = R * m12 + B * m32 + A * m42;
b = R * m13 + G * m23 + A * m43;
a = R * m14 + G * m24 + B * m34;

颜色旋转

颜色旋转的描述比较复杂,就是在图像像素中,用其中的2个分量按照一定的角度围绕另外1个分量作运算的结果,就是颜色的旋转。以红色分量R和绿色分量G围绕蓝色分量G旋转60度为例:

m11 = cos(60) = 0.5, m12 = sin(60) = 0.866, m21 = -sin(60) = -0.866, m22 = cos(60) = 0.5,那么,R和G 所得到的旋转量分别为:

scss 复制代码
r = R * m11(0.5) + G * m21(-0.866);
g = R * m12(0.866) + G * m22(0.5);

颜色平移

上面的缩放、剪切和旋转属于颜色的线性变换(都是乘法运算的累积和),而平移是颜色的非线性变换,就是对颜色各分量做一个加法而已:图像像素各分量的平移量用所谓的虚拟位,即第5行的各个值来表示,各分量加上所在列的虚拟行的值就是颜色平移,其实质就是非线性地调整了该分量的亮度值。用公式表示各分量的平移量:

ini 复制代码
r = R + m51 * 255;
g = G + m52 * 255;
b = B + m53 * 255;
a = A + m54 * 255;

白平衡

直方图

写在最后

岗位职责

  1. 负责剪映视频剪辑方向的研发、优化与架构设计工作;
  2. 负责剪映工具方向的研发,包含视频剪辑、导出、素材管理等模块,解决复杂剪辑操作及架构上所面临的严峻挑战,打造极致的工具体验;
  3. 负责项目重点、难点的技术攻坚任务,持续优化产品,提升产品质量;
  4. 共同建设抖音&西瓜的视频内容创作生态,在行业之间建立技术壁垒。

岗位要求

  1. 具备扎实的 Objective-C 或者 Swift 语言基础,熟悉常用的数据结构和算法;
  2. 熟悉 iOS 系统运行机制及内核,熟悉移动终端特性和解决方案;
  3. 优秀的编码习惯,对于设计模式等常见的编码技巧有很好的认知;
  4. 有较强的技术好奇心、自驱力,具备优秀的解决问题和逻辑思维能力;
  5. 有多媒体相关开发经验者优先,不要求有视频编辑开发经验。 |

简历投递链接: job.toutiao.com/s/idEgpQr7

相关推荐
Kaelinda2 小时前
iOS开发代码块-OC版
ios·xcode·oc
ii_best18 小时前
ios按键精灵自动化的脚本教程:自动点赞功能的实现
运维·ios·自动化
app开发工程师V帅1 天前
iOS 苹果开发者账号: 查看和添加设备UUID 及设备数量
ios
CodeCreator18181 天前
iOS AccentColor 和 Color Set
ios
iOS民工1 天前
iOS keychain
ios
m0_748238921 天前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Legendary_0082 天前
LDR6020在iPad一体式键盘的创新应用
ios·计算机外设·ipad
/**书香门第*/2 天前
Laya ios接入goole广告,搭建环境 1
ios
wakangda2 天前
React Native 集成 iOS 原生功能
react native·ios·cocoa
crasowas3 天前
iOS - 超好用的隐私清单修复脚本(持续更新)
ios·app store