IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    当我谈修图时,我谈些什么

    中文博客 on 范叶亮 | Leo Van发表于 2023-04-22 00:00:00
    love 0

    文本是「当我谈」系列的第一篇博客,后续「当我谈」系列会从程序员的视角一起科普认知未曾触及的其他领域。

    色彩空间

    色彩空间是对色彩的组织方式,借助色彩空间和针对物理设备的测试,可以得到色彩的固定模拟和数字表示。色彩模型是一种抽象数学模型,通过一组数字来描述颜色。由于“色彩空间”有着固定的色彩模型和映射函数组合,非正式场合下,这一词汇也被用来指代色彩模型。

    RGB

    红绿蓝(RGB)色彩模型,是一种加法混色模型,将红(Red)、绿(Green)、蓝(Blue)三原色的色光以不同的比例相加,以合成产生各种色彩光。三原色的原理不是出于物理原因,而是由于生理原因造成的。

    RGB 色彩模型可以映射到一个立方体上,如下图所示:

    红绿蓝的三原色光显示技术广泛用于电视和计算机的显示器,利用红、绿、蓝三原色作为子像素组成的真色彩像素,透过眼睛及大脑的模糊化,“人类看到”不存在于显示器上的感知色彩。

    CMYK

    印刷四分色模式(CMYK)是彩色印刷中采用的一种减法混色模型,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓的“全彩印刷”。四种标准颜色分别是:

    • Cyan:青色或“水蓝”
    • Magenta:洋红色或“紫色”
    • Yellow:黄色
    • Key plate:因实务上多使用黑色,所以也可以简单视为 blacK

    CMY 叠色的示意图如下所示:

    利用 $0$ 到 $1$ 的浮点数表示 $R, G, B$ 和 $C, M, Y, K$,从四分色向三原光转换公式如下:

    $$ \begin{aligned} R &= \left(1 - C\right) \left(1 - K\right) \\ G &= \left(1 - M\right) \left(1 - K\right) \\ B &= \left(1 - Y\right) \left(1 - K\right) \end{aligned} $$

    从三原光向四分色转换公式如下:

    $$ \begin{aligned} C &= 1 - \dfrac{R}{\max \left(R, G, B\right)} \\ M &= 1 - \dfrac{G}{\max \left(R, G, B\right)} \\ Y &= 1 - \dfrac{B}{\max \left(R, G, B\right)} \\ K &= 1 - \max \left(R, G, B\right) \\ \end{aligned} $$

    HSL 和 HSV

    HSL 和 HSV 都是一种将 RGB 色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构 RGB 更加直观。HSL 即色相、饱和度、亮度(Hue,Saturation,Lightness),HSV 即色相、饱和度、明度(Hue,Saturation,Value),又称 HSB,其中 B 为 Brightness。另种色彩空间定义如下图所示:

    HSL 和 HSV 色彩空间

    色相

    色相(Hue)指的是色彩的外相,是在不同波长的光照射下,人眼所感觉到的不同的颜色。在 HSL 和 HSV 色彩空间中,色相是以红色为 0 度(360 度)、黄色为 60 度、绿色为 120 度、青色为 180 度、蓝色为 240 度、洋红色为 300 度。如下图所示:

    饱和度

    饱和度(Saturation)指的是色彩的纯度,饱和度由光强度和它在不同波长的光谱中分布的程度共同决定。下图为红色从最小饱和度到最大饱和度的示例:

    亮度和明度

    明度值是与同样亮的白色物体相比,某物的亮的程度。如果我们拍摄一张图像,提取图像色相、饱和度和明度值,然后将它们与不同色彩空间的明度值进行比较,可以迅速地从视觉上得出差异。如下图所示,HSV 色彩空间中的 V 值和 HSL 色彩空间中的 L 值与感知明度值明显不同:

    原始图片
    HSL 中的 L
    HSV 中的 V

    差异

    HSV 和 HSL 两者对于色相(H)的定义一致,但对于饱和度(S)和亮度与明度(L 与 B)的定义并不一致。

    在 HSL 中,饱和度独立于亮度存在,也就是说非常浅的颜色和非常深的颜色都可以在 HSL 中非常饱和。而在 HSV 中,接近于白色的颜色都具有较低的饱和度。

    • HSV 中的 S 控制纯色中混入白色的量,值越大,混入的白色越少,颜色越纯。
    • HSV 中的 V 控制纯色中混入黑色的量,值越大,混入的黑色越少,明度越高。
    • HSL 中的 S 和黑白没有关系,饱和度不控制颜色中混入白色和黑色的多少。
    • HSL 中的 L 控制纯色中混入白色和黑色的多少。

    以 Photoshop 和 Afiinity Photo 两款软件的拾色器为例:

    Photoshop 拾色器(HSV)
    Afiinity Photo 拾色器(HSL)

    两个软件分别采用 HSV 和 HSL 色彩空间,其横轴为饱和度(S),纵轴分别为明度(V)和亮度(L)。不难看出,在 Photoshop 拾色器中,越往上混入的黑色越少,明度越高;越往右混入的白色越少,纯度越高。在 Afiinity Photo 拾色器中,下部为纯黑色,亮度最小,从下往上,混入的黑色逐渐减少,直到 50% 位置处完全没有黑色混入,继续往上走,混入的白色逐渐增加,直到 100% 位置处完全变为纯白色,亮度最高。

    直方图

    图像直方图是反映图像色彩亮度的直方图,其中 $x$ 轴表示亮度值,$y$ 轴表示图像中该亮度值像素点的个数。以 $8$ 位图像为例,亮度的取值范围为 $\left[0, 2^8-1\right]$,即 $\left[0, 255\right]$。以如下图片为例(原始图片:链接):

    原始图片

    在 Lightroom 中直方图如下所示:

    原始图片 Lightroom 直方图

    利用 Python 绘制的直方图如下所示:

    直方图代码
    import cv2
    
    import numpy as np
    import matplotlib.pyplot as plt
    
    gray_img = cv2.imread('demo.jpg', cv2.IMREAD_GRAYSCALE)
    img = cv2.imread('demo.jpg')
    img_channels = cv2.split(img)
    height, width = gray_img.shape
    gray_img_hist = cv2.calcHist([gray_img], [0], None, [256], [0, 256])
    img_channels_hist = [cv2.calcHist([img_channel], [0], None, [256], [0, 256])
                         for img_channel in img_channels]
    
    fig, ax = plt.subplots(1, 1)
    
    ax.plot(gray_img_hist, color='0.6', label='灰')
    
    for (img_channel_hist, color, label) in zip(
      img_channels_hist, ['#6695ff', '#70df5f', '#f74048'], ['蓝', '绿', '红']):
        ax.plot(img_channel_hist, color=color, label=label)
    
    segments = [0, 28, 85, 170, 227, 255]
    segments_text = ['黑色', '阴影', '曝光', '高光', '白色']
    
    for (left_border, right_border, segment_text) in \
            zip(segments[:-1], segments[1:], segments_text):
      if left_border != 0:
        ax.axvline(x=left_border, ymin=0, color='black')
      
      ax.annotate(
          segment_text,
          xy=((left_border + right_border) / 2, np.max(img_channels_hist) / 3),
          ha='center')
    
    ax.legend(loc='upper center')
    plt.xlim([0, 256])
    ax.set_xticks([0, 32, 64, 96, 128, 160, 192, 224, 256])
    ax.axes.get_yaxis().set_visible(False)
    
    plt.tight_layout()
    fig.set_size_inches(8, 4)
    plt.savefig('demo-image-histgram.png', dpi=100)
    
    原始图片直方图

    直方图以 $28, 85, 170, 227$ 为分界线可以划分为黑色、阴影、曝光、高光、白色共 5 个区域。其中曝光区域以适中的亮度保留了图片最多的细节,阴影和高光对应了照片中较暗和较亮的区域,黑色和白色两个部分则几乎没有任何细节。当整个直方图过于偏左时表示欠曝,过于偏右时则表示过曝。

    色温

    色温(Temperature)是指照片中光源发出相似的光的黑体辐射体所具有的开尔文温度。开尔文温度越低光越暖,开尔文温度越高光越冷,如下图所示:

    针对图片分别应用 5000K 和 10000K 色温的对比结果如下图所示:

    色温代码
    import math
    import cv2
    
    import numpy as np
    
    
    def __kelvin_to_rgb(kelvin: int) -> (int, int, int):
      kelvin = np.clip(kelvin, min_val=1000, max_val=40000)
      temperature = kelvin / 100.0
    
      # 红色通道
      if temperature < 66.0:
          red = 255
      else:
          # a + b x + c Log[x] /.
          # {a -> 351.97690566805693`,
          # b -> 0.114206453784165`,
          # c -> -40.25366309332127
          # x -> (kelvin/100) - 55}
          red = temperature - 55.0
          red = 351.97690566805693 + 0.114206453784165 * red \
                - 40.25366309332127 * math.log(red)
    
      # 绿色通道
      if temperature < 66.0:
          # a + b x + c Log[x] /.
          # {a -> -155.25485562709179`,
          # b -> -0.44596950469579133`,
          # c -> 104.49216199393888`,
          # x -> (kelvin/100) - 2}
          green = temperature - 2
          green = -155.25485562709179 - 0.44596950469579133 * green \
                  + 104.49216199393888 * math.log(green)
      else:
          # a + b x + c Log[x] /.
          # {a -> 325.4494125711974`,
          # b -> 0.07943456536662342`,
          # c -> -28.0852963507957`,
          # x -> (kelvin/100) - 50}
          green = temperature - 50.0
          green = 325.4494125711974 + 0.07943456536662342 * green \
                  - 28.0852963507957 * math.log(green)
    
      # 蓝色通道
      if temperature >= 66.0:
          blue = 255
      elif temperature <= 20.0:
          blue = 0
      else:
          # a + b x + c Log[x] /.
          # {a -> -254.76935184120902`,
          # b -> 0.8274096064007395`,
          # c -> 115.67994401066147`,
          # x -> kelvin/100 - 10}
          blue = temperature - 10.0
          blue = -254.76935184120902 + 0.8274096064007395 * blue \
                 + 115.67994401066147 * math.log(blue)
    
      return np.clip(red, 0, 255), np.clip(green, 0, 255), np.clip(blue, 0, 255)
    
    
    def __mix_color(v1, v2, ratio: float):
      return np.array((1.0 - ratio) * v1 + 0.5).astype(np.uint8) \
          + np.array(ratio * v2).astype(np.uint8)
    
    
    def __keep_original_lightness(original_image, image):
      original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1]
      h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS))
    
      return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR)
    
    
    def apply_temperature(
            image,
            temperature,
            keep_original_lightness: bool = True):
      b, g, r = cv2.split(image)
      n_b = np.clip(b.astype(np.single) - temperature, 0, 255).astype(np.uint8)
      n_r = np.clip(r.astype(np.single) + temperature, 0, 255).astype(np.uint8)
      ret_image = cv2.merge([n_b, g, n_r])
    
      return __keep_original_lightness(image, ret_image) \
          if keep_original_lightness else ret_image
    
    
    def apply_kelvin(
            image,
            kelvin: int,
            strength: float = 0.6,
            keep_original_lightness: bool = True):
      b, g, r = cv2.split(image)
      k_r, k_g, k_b = __kelvin_to_rgb(kelvin)
      n_r, n_g, n_b = __mix_color(r, k_r, strength), \
          __mix_color(g, k_g, strength), __mix_color(b, k_b, strength)
      ret_image = cv2.merge([n_b, n_g, n_r])
    
      return __keep_original_lightness(image, ret_image) \
          if keep_original_lightness else ret_image
    
    
    img = cv2.imread('demo.jpg')
    cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 5000))
    cv2.imwrite('demo-color-temperature-cold.jpg', apply_kelvin(img, 10000))
    

    色调

    色调(Tint)允许我们为了实现中和色偏或增加色偏的目的,而将色偏向绿色或洋红色转变。针对图片分别应用 -30 和 +30 色调的对比结果如下图所示:

    色调代码
    import cv2
    
    import numpy as np
    
    
    def __keep_original_lightness(original_image, image):
      original_l = cv2.cvtColor(original_image, cv2.COLOR_BGR2HLS)[..., 1]
      h, l, s = cv2.split(cv2.cvtColor(image, cv2.COLOR_BGR2HLS))
    
      return cv2.cvtColor(cv2.merge([h, original_l, s]), cv2.COLOR_HLS2BGR)
    
    
    def apply_tint(image, tint, keep_original_lightness: bool = True):
      b, g, r = cv2.split(image)
      n_g = np.clip(g.astype(np.single) + tint, 0, 255).astype(np.uint8)
      ret_image = cv2.merge([b, n_g, r])
    
      return __keep_original_lightness(image, ret_image) \
        if keep_original_lightness else ret_image
    
    
    img = cv2.imread('demo.jpg')
    cv2.imwrite('demo-color-tint-negative.jpg', apply_tint(img, -30))
    cv2.imwrite('demo-color-tint-positive.jpg', apply_tint(img, +30))
    



沪ICP备19023445号-2号
友情链接