复制 Photoshop 中的sRGB到LAB转换

6

我想要实现的任务是复制 Photoshop 的 RGB 到 LAB 转换。

为了简单起见,我将描述我提取仅 L 通道的方法。

提取 Photoshop 的 L 通道

这是包含所有 RGB 颜色的 RGB 图像(请点击并下载):

RGB Colors Image

为了提取Photoshop的LAB,我所做的是以下步骤:
  1. 将图像加载到Photoshop中。
  2. 将模式设置为LAB。
  3. 在通道面板中选择L通道。
  4. 将模式设置为灰度。
  5. 将模式设置为RGB。
  6. 保存为PNG格式。
这是Photoshop的L通道(当在LAB模式下选择L通道时,屏幕上会看到这个):

Photoshop's L Channel Image

sRGB到LAB的转换

我主要参考Bruce Lindbloom的网站
还有一个已知的事实是Photoshop在其LAB模式中使用D50白点(另请参阅维基百科的LAB色彩空间页面)。

假设RGB图像采用sRGB格式,则转换如下:

sRGB -> XYZ(白点D65) -> XYZ(白点D50) -> LAB

假设数据在[0, 1]范围内的浮点数,则各阶段如下:

  1. 将sRGB转换为XYZ
    转换矩阵由RGB -> XYZ矩阵给出(请参见sRGB D65)。
  2. 从XYZ D65转换到XYZ D50
    使用色适应矩阵进行转换。由于前一步和这一步都是矩阵乘法,因此它们可以合并成一个矩阵,该矩阵从sRGB -> XYZ D50(请参见RGB to XYZ Matrix底部)。请注意,Photoshop使用Bradford适应方法。
  3. 从XYZ D50转换为LAB
    使用XYZ to LAB步骤进行转换。

MATLAB 代码

首先,我只需要 L 通道,所以事情变得简单了一些。图像被加载到 MATLAB 中,并转换为浮点数 [0, 1] 范围内。

以下是代码:

%% Setting Enviorment Parameters

INPUT_IMAGE_RGB             = 'RgbColors.png';
INPUT_IMAGE_L_PHOTOSHOP     = 'RgbColorsL.png';


%% Loading Data

mImageRgb   = im2double(imread(INPUT_IMAGE_RGB));
mImageLPhotoshop     = im2double(imread(INPUT_IMAGE_L_PHOTOSHOP));
mImageLPhotoshop     = mImageLPhotoshop(:, :, 1); %<! All channels are identical


%% Convert to L Channel

mImageLMatlab = ConvertRgbToL(mImageRgb, 1);


%% Display Results
figure();
imshow(mImageLPhotoshop);
title('L Channel - Photoshop');

figure();
imshow(mImageLMatlab);
title('L Channel - MATLAB');

函数ConvertRgbToL()定义如下:

function [ mLChannel ] = ConvertRgbToL( mRgbImage, sRgbMode )

OFF = 0;
ON  = 1;

RED_CHANNEL_IDX     = 1;
GREEN_CHANNEL_IDX   = 2;
BLUE_CHANNEL_IDX    = 3;

RGB_TO_Y_MAT = [0.2225045, 0.7168786, 0.0606169]; %<! D50

Y_CHANNEL_THR = 0.008856;

% sRGB Compensation
if(sRgbMode == ON)
    vLinIdx = mRgbImage < 0.04045;

    mRgbImage(vLinIdx)  = mRgbImage(vLinIdx) ./ 12.92;
    mRgbImage(~vLinIdx) = ((mRgbImage(~vLinIdx) + 0.055) ./ 1.055) .^ 2.4;
end

% RGB to XYZ (D50)
mY = (RGB_TO_Y_MAT(1) .* mRgbImage(:, :, RED_CHANNEL_IDX)) + (RGB_TO_Y_MAT(2) .* mRgbImage(:, :, GREEN_CHANNEL_IDX)) + (RGB_TO_Y_MAT(3) .* mRgbImage(:, :, BLUE_CHANNEL_IDX));

vYThrIdx = mY > Y_CHANNEL_THR;

mY3 = mY .^ (1 / 3);

mLChannel = ((vYThrIdx .* (116 * mY3 - 16.0)) + ((~vYThrIdx) .* (903.3 * mY))) ./ 100;


end

可以看到结果不同。
对于大多数颜色,Photoshop要暗得多。

有人知道如何复制Photoshop的LAB转换吗?
有人能发现这段代码中的问题吗?

谢谢。


也许你关于Photoshop如何完成工作的参考资料不正确。由于它是专有软件,只有他们才能回答这个问题。 - Ander Biguri
1
不清楚具体要遵循哪些步骤。我会尝试分阶段验证/比较,找出结果差异的地方。例如,你确定灰度转换是相同的吗?当你说它是“float”时,你确定它是双精度而不是单精度吗?我也只会查看文件/数据中的值,而不是你在屏幕上看到的值。你尝试过Matlab的rgb2lab和类似的函数吗? - horchler
此外,您的一些资源似乎已经过时了几年。我们不知道在此期间Photoshop是否已经进行了修改。您使用的是哪个版本的Photoshop和Matlab? - horchler
1
无论是MATLAB内部的RGB到LAB转换,还是任何在MATLAB文件交换平台上的文件,在进行以上计算时都没有采用sRGB Gamma补偿和D65白点。即使是使用D50白点的MATLAB也无法与Photoshop匹配。 - Royi
尝试反转我的代码来重写你的代码,这样你就能得到你想要的结果。 - adrienlucca.net
显示剩余2条评论
2个回答

1
所有在Photoshop中颜色空间之间的转换都是通过CMM进行的,它在大约2000年的硬件上足够快速,但不太精确。如果您勾选“轮换”- RGB -> Lab -> RGB,可能会出现许多4位错误和一些7位错误的Adobe CMM。这可能会导致海报化。我总是基于公式而不是基于CMM进行转换。然而,Adobe CMM和Argyll CMM的平均deltaE误差是相当可接受的。
Lab转换与RGB非常相似,只是在第一步应用了非线性(gamma);类似于以下内容:
1.将XYZ归一化到白点 2.将结果带到gamma 3(保持阴影部分线性,取决于实现) 3.将结果乘以[0 116 0 -16; 500 -500 0 0; 0 200 -200 0]'

1

最新答案(我们知道它是错误的,正在等待正确的答案)

Photoshop 是一款非常古老且混乱的软件。关于为什么在执行从一种模式到另一种模式的转换时像素值会发生这样或那样的变化,没有清晰的文档说明。

您遇到的问题是因为当您将所选的 L* 通道转换为 Adobe Photoshop 中的 Greyscale 时,伽马值会发生变化。原生地,转换使用 1.74 的伽马值进行单通道到灰度转换。不要问我为什么,我猜这可能与旧激光打印机有关(?)。

无论如何,以下是我发现的最佳方法:

打开您的文件,将其转换为 LAB 模式,仅选择 L 通道

然后去:

编辑 > 转换颜色配置文件

您将选择“自定义伽马”并输入值 2.0(不要问我为什么 2.0 的效果更好,我不知道 Adobe 的软件制造商在想什么...) 此操作将使您的图片变成仅具有一个通道的灰度图像

然后您可以将其转换为 RGB 模式。

如果您将结果与自己的结果进行比较,您会看到差异高达4点多个百分点 - 所有差异都位于最暗的区域。

我怀疑这是因为伽马曲线应用不适用于LAB模式中的暗值(如您所知,所有低于0.008856的XYZ值在LAB中都是线性的)

结论:

据我所知,Adobe Photoshop中没有适当实现的方法可以从LAB模式提取L通道到灰度模式!

先前答案

这是我使用自己的方法得到的结果:

RGB2LAB

看起来与 Adobe Photoshop 的结果完全相同。

我不确定你的问题出在哪里,因为你描述的步骤与我所遵循的完全相同,也是我建议你遵循的步骤。我没有 Matlab,所以我使用了 Python:

import cv2, Syn

# your file
fn = "EASA2.png"

#reading the file
im = cv2.imread(fn,-1)

#openCV works in BGR, i'm switching to RGB
im = im[:,:,::-1]

#conversion to XYZ
XYZ = Syn.sRGB2XYZ(im)

#white points D65 and D50
WP_D65 = Syn.Yxy2XYZ((100,0.31271, 0.32902))
WP_D50 = Syn.Yxy2XYZ((100,0.34567, 0.35850))

#bradford
XYZ2 = Syn.bradford_adaptation(XYZ, WP_D65, WP_D50) 

#conversion to L*a*b*
LAB = Syn.XYZ2Lab(XYZ2, WP_D50)

#picking the L channel only
L = LAB[:,:,0] /100. * 255.

#image output
cv2.imwrite("result.png", L)

Syn库是我自己的东西,这里是函数列表(抱歉有些凌乱):

def sRGB2XYZ(sRGB):

    sRGB = np.array(sRGB)
    aShape = np.array([1,1,1]).shape
    anotherShape = np.array([[1,1,1],[1,1,1]]).shape
    origShape = sRGB.shape

    if sRGB.shape == aShape:
        sRGB = np.reshape(sRGB, (1,1,3))

    elif len(sRGB.shape) == len(anotherShape):
        h,d = sRGB.shape
        sRGB = np.reshape(sRGB, (1,h,d))

    w,h,d = sRGB.shape

    sRGB = np.reshape(sRGB, (w*h,d)).astype("float") / 255.

    m1 = sRGB[:,0] > 0.04045
    m1b = sRGB[:,0] <= 0.04045
    m2 = sRGB[:,1] > 0.04045
    m2b = sRGB[:,1] <= 0.04045
    m3 = sRGB[:,2] > 0.04045
    m3b = sRGB[:,2] <= 0.04045

    sRGB[:,0][m1] = ((sRGB[:,0][m1] + 0.055 ) / 1.055 ) ** 2.4
    sRGB[:,0][m1b] = sRGB[:,0][m1b] / 12.92

    sRGB[:,1][m2] = ((sRGB[:,1][m2] + 0.055 ) / 1.055 ) ** 2.4
    sRGB[:,1][m2b] = sRGB[:,1][m2b] / 12.92

    sRGB[:,2][m3] = ((sRGB[:,2][m3] + 0.055 ) / 1.055 ) ** 2.4
    sRGB[:,2][m3b] = sRGB[:,2][m3b] / 12.92

    sRGB *= 100. 

    X = sRGB[:,0] * 0.4124 + sRGB[:,1] * 0.3576 + sRGB[:,2] * 0.1805
    Y = sRGB[:,0] * 0.2126 + sRGB[:,1] * 0.7152 + sRGB[:,2] * 0.0722
    Z = sRGB[:,0] * 0.0193 + sRGB[:,1] * 0.1192 + sRGB[:,2] * 0.9505

    XYZ = np.zeros_like(sRGB)

    XYZ[:,0] = X
    XYZ[:,1] = Y
    XYZ[:,2] = Z

    XYZ = np.reshape(XYZ, origShape)

    return XYZ

def Yxy2XYZ(Yxy):

    Yxy = np.array(Yxy)
    aShape = np.array([1,1,1]).shape
    anotherShape = np.array([[1,1,1],[1,1,1]]).shape
    origShape = Yxy.shape

    if Yxy.shape == aShape:
        Yxy = np.reshape(Yxy, (1,1,3))

    elif len(Yxy.shape) == len(anotherShape):
        h,d = Yxy.shape
        Yxy = np.reshape(Yxy, (1,h,d))

    w,h,d = Yxy.shape

    Yxy = np.reshape(Yxy, (w*h,d)).astype("float")

    XYZ = np.zeros_like(Yxy)

    XYZ[:,0] = Yxy[:,1] * ( Yxy[:,0] / Yxy[:,2] )
    XYZ[:,1] = Yxy[:,0]
    XYZ[:,2] = ( 1 - Yxy[:,1] - Yxy[:,2] ) * ( Yxy[:,0] / Yxy[:,2] )

    return np.reshape(XYZ, origShape)

def bradford_adaptation(XYZ, Neutral_source, Neutral_destination):
    """should be checked if it works properly, but it seems OK"""

    XYZ = np.array(XYZ)
    ashape = np.array([1,1,1]).shape
    siVal = False

    if XYZ.shape == ashape:


        XYZ = np.reshape(XYZ, (1,1,3))
        siVal = True


    bradford = np.array(((0.8951000, 0.2664000, -0.1614000),
                          (-0.750200, 1.7135000,  0.0367000),
                          (0.0389000, -0.068500,  1.0296000)))

    inv_bradford = np.array(((0.9869929, -0.1470543, 0.1599627),
                              (0.4323053,  0.5183603, 0.0492912),
                              (-.0085287,  0.0400428, 0.9684867)))

    Xs,Ys,Zs = Neutral_source
    s = np.array(((Xs),
                   (Ys),
                   (Zs)))

    Xd,Yd,Zd = Neutral_destination
    d = np.array(((Xd),
                   (Yd),
                   (Zd)))


    source = np.dot(bradford, s)
    Us,Vs,Ws = source[0], source[1], source[2]

    destination = np.dot(bradford, d)
    Ud,Vd,Wd = destination[0], destination[1], destination[2]

    transformation = np.array(((Ud/Us, 0, 0),
                                (0, Vd/Vs, 0),
                                (0, 0, Wd/Ws)))

    M = np.mat(inv_bradford)*np.mat(transformation)*np.mat(bradford)

    w,h,d = XYZ.shape
    result = np.dot(M,np.rot90(np.reshape(XYZ, (w*h,d)),-1))
    result = np.rot90(result, 1)
    result = np.reshape(np.array(result), (w,h,d))

    if siVal == False:
        return result
    else:
        return result[0,0]

def XYZ2Lab(XYZ, neutral):
    """transforms XYZ to CIE Lab
    Neutral should be normalized to Y = 100"""

    XYZ = np.array(XYZ)
    aShape = np.array([1,1,1]).shape
    anotherShape = np.array([[1,1,1],[1,1,1]]).shape
    origShape = XYZ.shape

    if XYZ.shape == aShape:
        XYZ = np.reshape(XYZ, (1,1,3))

    elif len(XYZ.shape) == len(anotherShape):
        h,d = XYZ.shape
        XYZ = np.reshape(XYZ, (1,h,d))

    N_x, N_y, N_z = neutral
    w,h,d = XYZ.shape

    XYZ = np.reshape(XYZ, (w*h,d)).astype("float")

    XYZ[:,0] = XYZ[:,0]/N_x
    XYZ[:,1] = XYZ[:,1]/N_y
    XYZ[:,2] = XYZ[:,2]/N_z

    m1 = XYZ[:,0] > 0.008856
    m1b = XYZ[:,0] <= 0.008856
    m2 = XYZ[:,1] > 0.008856 
    m2b = XYZ[:,1] <= 0.008856
    m3 = XYZ[:,2] > 0.008856
    m3b = XYZ[:,2] <= 0.008856

    XYZ[:,0][m1] = XYZ[:,0][XYZ[:,0] > 0.008856] ** (1/3.0)
    XYZ[:,0][m1b] = ( 7.787 * XYZ[:,0][m1b] ) + ( 16 / 116.0 )

    XYZ[:,1][m2] = XYZ[:,1][XYZ[:,1] > 0.008856] ** (1/3.0)
    XYZ[:,1][m2b] = ( 7.787 * XYZ[:,1][m2b] ) + ( 16 / 116.0 )

    XYZ[:,2][m3] = XYZ[:,2][XYZ[:,2] > 0.008856] ** (1/3.0)
    XYZ[:,2][m3b] = ( 7.787 * XYZ[:,2][m3b] ) + ( 16 / 116.0 )

    Lab = np.zeros_like(XYZ)

    Lab[:,0] = (116. * XYZ[:,1] ) - 16.
    Lab[:,1] = 500. * ( XYZ[:,0] - XYZ[:,1] )
    Lab[:,2] = 200. * ( XYZ[:,1] - XYZ[:,2] )

    return np.reshape(Lab, origShape)

2
我想我明白这里发生了什么。我在MATLAB中比较了值。由于我在Windows上有Display ICC,难道Photoshop写入的PNG具有不同的值吗?我的屏幕配置文件是否会更改Photoshop写入PNG的值? - Royi
1
让我重新表述一下,假设我加载的是sRGB(所有颜色图像)PNG,将其转换为LAB并进行所有处理,然后将其保存为PNG。在不同的显示配置文件的情况下,该PNG的值是否会有所不同? - Royi
我不认为会出现这种情况,但是可能会发生的是Photoshop界面中显示的值会改变。但是文件中像素的值,我不认为会改变。顺便说一下,我重新检查了一下,我得到的值与Adobe的略有不同,但在sRGB中从未超过2/255。 - adrienlucca.net
你顺便问一下,为什么需要那个? - adrienlucca.net
1
并不完全准确。当两者在Photoshop上加载到屏幕上时,它们是相同的。但是如果我执行以下操作:mA = im2double(imread('40qEJq4.jpg')); mB = im2double(imread('K7ftH.png')); mE = mA - mB; figure(); plot(mE(:)); 这是结果- http://i.imgur.com/oaN8FwA.png。您可以看到有高达10%的差异。再次,您得到了与我一样的结果。在Photoshop中它们是相同的(您引起了我的注意),但编码到PNG中的值是不同的。因此,我想知道是否拥有屏幕配置文件会改变写入PNG文件的值。 - Royi
显示剩余11条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接