annidy / notes

1 stars 0 forks source link

颜色空间 #149

Open annidy opened 10 months ago

annidy commented 10 months ago

什么是颜色空间

颜色空间是定义颜色显示范围。

早期(目前也是)的显示设备由于技术限制,无法显示出所有颜色。以灰阶为例,电视显示的范围是16-235,超出这个范围的值只能做削波处理,导致某些颜色显示不出来。因此在编码时,我们可以把源信号从[0-255],转换到[16-235]内,相当于把溢出的部分均匀分摊到中间位置。虽然这样中间部分的颜色显示不准,但起码显示颜色的范围是完整的。但是具体到不同设备,比如电视,虽然BT.709色彩空间规范定义显示范围是16-235,但实际不同显示器仍然有色差。有的显示器颜色范围更大,同一个文件往往能看到更多细节。

色域

image 色域规定了显示颜色范围(上一节以灰度举例,但是对于彩色显示设备,不同颜色显示范围并不一样)。比如BT.709就是一种色域。每种色域主要影响的是RGB与YUV/YCbCr两种颜色的转换系数。 以BT.709为例,标准规定的转换是: Y = 0.2126 R + 0.7152 G + 0.0722 B Cb = (B - Y) / 1.8556 = -0.1146 R - 0.3854 G + 0.5 B + 128 Cr = (R - Y) / 1.5748 = 0.5 R - 0.4542 G - 0.0490 B + 128 而BT.601的转换是 Y = 0.299 R + 0.587 G + 0.114 B Cb = (B - Y) / 1.772 = -0.1687 R - 0.3313 G + 0.5 B + 128 Cr = (R - Y) / 1.402 = 0.5 R - 0.4197 G - 0.0813 B + 128

反过来也有应于公式,实际在计算时是通过矩阵已经逆矩阵完成运算,这里不再赘述。 由于转换都是浮点计算,会出现取整、溢出等。所以转换并不是无损的,每个颜色的位数也对结果有影响。默认是8bit,有些格式是10bit,转化的精度自然就高一些。

如何查看颜色空间

事实上,在生成文件后,都会嵌入对应的颜色空间信息。可以通过ffprobe工具打印。(推荐mediainfo,更专业)

ffprobe v1_1080p.mov

Stream #0:1[0x2](und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, bt709), 720x1280, 6545 kb/s, 30 fps, 30 tbr, 15360 tbn (default)
    Metadata:
      creation_time   : 2024-01-03T09:21:45.000000Z
      handler_name    : Core Media Video
      vendor_id       : [0][0][0][0]
      encoder         : HEVC

图片也有

ffprobe a.jpg

  Duration: 00:00:00.04, start: 0.000000, bitrate: 9712 kb/s
  Stream #0:0: Video: mjpeg (Baseline), yuvj444p(pc, bt470bg/unknown/unknown), 178x316 [SAR 1:1 DAR 89:158], 25 fps, 25 tbr, 25 tbn

括号中的是颜色矩阵和颜色转换空间。比如

广色域图片

ffplay file.png

Input #0, png_pipe
, from 'file.png':
  Duration: N/A, bitrate: N/A
  Stream #0:0: Video: png, rgb24(pc), 640x640 [SAR 2835:2835 DAR 1:1], 25 fps, 25 tbr, 25 tbn

系统播放器在显示这些文件时,通常都会根据文件信息中的颜色空间,结合硬件(显示器)的显示范围(色域)进行自动调整。 代码处理相对繁琐一些,通常需要借助系统API实现。

Instagram 如何在新 iPhone 的广色域屏幕中显示广色域图像

使用mediainfo查看图片intro_1

Image
Format                                   : PNG
Format/Info                              : Portable Network Graphic
Compression                              : Deflate
Width                                    : 1 080 pixels
Height                                   : 1 920 pixels
Display aspect ratio                     : 0.562
Color space                              : RGB
Bit depth                                : 8 bits
Compression mode                         : Lossless
Stream size                              : 1.12 MiB (100%)
Color range                              : Full
Color primaries                          : BT.709
Transfer characteristics                 : BT.709
Matrix coefficients                      : Identity
Gamma                                    : 0.510

颜色空间转换(CSC)

ffmpeg可以通过libswscale从一个颜色格式转换到另一个颜色格式。

AV_PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

AV_PIX_FMT_YUVJ420P,  ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range

YUV420P对应的是(tv, bt709),YUVJ420P对应的是(pc, bt601). AV_PIX_FMT_YUV420P与AV_PIX_FMT_YUVJ420P

下面是一个示例代码:

#include <libswscale/swscale.h>

int main() {
    // 输入图像参数
    int src_width = 1920;
    int src_height = 1080;
    enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_YUV420P;

    // 输出图像参数
    int dst_width = 1920;
    int dst_height = 1080;
    enum AVPixelFormat dst_pix_fmt = AV_PIX_FMT_YUVJ420P;

    // 创建图像转换上下文
    SwsContext* swsContext = sws_getContext(src_width, src_height, src_pix_fmt,
                                            dst_width, dst_height, dst_pix_fmt,
                                            SWS_BICUBIC, NULL, NULL, NULL);
    if (!swsContext) {
        // 错误处理
        return -1;
    }

    // 输入图像数据
    uint8_t* src_data[4] = { nullptr };
    int src_linesize[4] = { 0 };
    int src_stride = src_width;

    // 输出图像数据
    uint8_t* dst_data[4] = { nullptr };
    int dst_linesize[4] = { 0 };
    int dst_stride = dst_width;

    // 转换图像
    sws_scale(swsContext, src_data, src_linesize, 0, src_height,
              dst_data, dst_linesize);

    // 释放资源
    sws_freeContext(swsContext);

    return 0;
}

如果是yuv和rgb的转换,还需要设置色域等信息

   sws_setColorspaceDetails(swsContext, sws_getCoefficients(SWS_CS_ITU709), 0,
                    sws_getCoefficients(SWS_CS_DEFAULT), 0, 0, 1 << 16, 1 << 16);

函数说明

int sws_setColorspaceDetails(struct SwsContext *c,
const int   inv_table[4],
int     srcRange,
const int   table[4],
int     dstRange,
int     brightness,
int     contrast,
int     saturation 
)       
Parameters
dstRange    flag indicating the while-black range of the output (1=jpeg / 0=mpeg)
srcRange    flag indicating the while-black range of the input (1=jpeg / 0=mpeg)
table   the yuv2rgb coefficients describing the output yuv space, normally ff_yuv2rgb_coeffs[x]
inv_table   the yuv2rgb coefficients describing the input yuv space, normally ff_yuv2rgb_coeffs[x]
brightness  16.16 fixed point brightness correction
contrast    16.16 fixed point contrast correction
saturation  16.16 fixed point saturation correction

sRGB颜色空间 vs 线性颜色空间 vs Gamma颜色空间

老式的CRT显示器,颜色输入和输出并不是线性显示的,而是一条曲线 image 用公式来表示就是Y = X ^ G,X = 输入,Y = 输出,G = Gamma系数。默认这个是2.2。现代的液晶显示器是可以达到线性输出的,但有些模式为了兼容老式CRT,也内置了Gamma校准。

这当然不是我们期望的效果,我们期望输入0.5,实际显示的也是0.5。但是在CRT显示器中,大部分输出都比输入暗,因此画面整体会偏暗。

为了补偿CRT显示问题,人为的对图片进行反向提亮操作,即sRGB空间。提亮的系数一般为0.45,这样刚好抵消显示器Gamma,达到理想中的线性输出。但是如果输出的是线性颜色空间,会出现过曝的现象,这时必须对每个颜色pow(x, 2.2)还原到线性空间。

annidy commented 10 months ago

每个人都需要知道的色彩理论|色彩空间|色彩模型|PS基础