## Steve on Image Processing with MATLABImage processing concepts, algorithms, and MATLAB

Note

Steve on Image Processing with MATLAB has been archived and will not be updated.

# Converting from sRGB to SWOP CMYK

An Image Processing Toolbox user recently reported to us good results for using makecform and applycform to convert colors from sRGB to CMYK. Here's the code they used:

c = makecform('srgb2cmyk');
cmyk_colors = applycform(srgb_colors,c);

The user asked if we could provide formulas for the conversion, but we could not. The conversion process involves several computation steps. One of the steps is a specialized interpolation involving a four-dimensional lookup table, for which there is no formula.

If one is very familiar with the internals of makecform and applycform, as well as with the organization of code within the toolbox, one could work out the exact steps of the computation. But that's only about four people. So, I thought I would decode the steps here and take you through it.

Before diving in too deep, though, I first want to observe that there is no single standard way to convert to CMYK. That's because the conversion depends strongly on several fundamental factors, including:

• the reflectance properties of a specific set of ink pigments
• how the spread of ink dots on a particular type of paper
• heuristics that control exactly where and how to lay down the black (K) ink
• how to adjust input colors that cannot be exactly reproduced in the output CMYK space

These factors make the conversion depend on the characteristics of a specific device.

If that's the case, though, then what is applycform doing? It is performing conversion based on the information contained in two ICC profiles, sRGB.icm and swopcmyk.icm. ICC stands for International Color Consortium, and an ICC profile contains computational recipes for converting device colors (or a standard color space such as sRGB) to a common working color space, called a connection space. The connection space is either CIEXYZ or CIELAB.

Here's a peek at the contents of sRGB.icm:

srgb_profile = iccread('sRGB.icm');

ans =

struct with fields:

Size: 3124
CMMType: 'appl'
Version: '2.1.0'
DeviceClass: 'display'
ColorSpace: 'RGB'
ConnectionSpace: 'XYZ'
CreationDate: '03-Dec-1999 17:30:00'
Signature: 'acsp'
PrimaryPlatform: 'Apple'
Flags: 0
IsEmbedded: 0
IsIndependent: 1
DeviceManufacturer: 'IEC '
DeviceModel: 'sRGB'
Attributes: 0
IsTransparency: 0
IsMatte: 0
IsNegative: 0
IsBlackandWhite: 0
RenderingIntent: 'perceptual'
Illuminant: [0.9642 1 0.8249]
Creator: 'HP  '



This profile specifies the computations for converting between sRGB and CIEXYZ.

Here's a look at the second profile, swopcmyk.icm:

cmyk_profile = iccread('swopcmyk.icm');

ans =

struct with fields:

Size: 503780
CMMType: 'appl'
Version: '4.0.0'
DeviceClass: 'output'
ColorSpace: 'CMYK'
ConnectionSpace: 'Lab'
CreationDate: '14-Apr-2006 02:00:00'
Signature: 'acsp'
PrimaryPlatform: 'Apple'
Flags: 131072
IsEmbedded: 0
IsIndependent: 1
DeviceManufacturer: '    '
DeviceModel: '    '
Attributes: 0
IsTransparency: 0
IsMatte: 0
IsNegative: 0
IsBlackandWhite: 0
RenderingIntent: 'relative colorimetric'
Illuminant: [0.9642 1 0.8249]
Creator: '    '
ProfileID: [1×16 uint8]



This profile specifies the computations for converting between something called SWOP CMYK and CIELAB. SWOP (Specifications for Web Offset Printing), now part of Idealliance, was a U.S. organization that created common industry specifications that are widely used in the U.S. print processes and materials.

Enough background. To see how the computation is actually performed, we need to dig (deep) into the internals of the struct returned by makecform.

cform = makecform('srgb2cmyk')

cform =

struct with fields:

c_func: @applyiccsequence
ColorSpace_in: 'RGB'
ColorSpace_out: 'CMYK'
encoding: 'uint16'
cdata: [1×1 struct]



Notice the function handle, @applyiccsequence. Most of the function handles we're going to see are for "private" toolbox functions. That means they are not directly callable, but you can look inside them to see what they're doing. Use which -all to see where applyiccsequence is located.

which -all applyiccsequence

/Applications/MATLAB_R2018b.app/toolbox/images/colorspaces/private/applyiccsequence.m  % Private to colorspaces


Then you can type

edit private/applyiccsequence

to load the MATLAB source file into the editor for your inspection. This particular source file is not very interesting; it just applies a sequence of computational steps. The data used by the function handle is in the cdata field of the struct.

cform.cdata.sequence

ans =

struct with fields:

source: [1×1 struct]
fix_black: [1×1 struct]
fix_pcs: [1×1 struct]
destination: [1×1 struct]



The four fields mentioned represent four steps:

1. Convert from the "source" colorspace (sRGB) to the connection colorspace (which is CIEXYZ for the sRGB.icm profile).
2. Apply a blackpoint adjustment step that is needed because the two ICC profiles have different versions.
3. Convert from CIEXYZ to CIELAB, which is needed because the two profiles are using different connection spaces.
4. Convert from CIELAB to the "destination" colorspace (SWOP CMYK).

#### Step 1: Convert from source colorspace to CIEXYZ

This step is achieved using the private function applymattrc_fwd and parameters stored in the nested cdata struct.

cform.cdata.sequence.source

ans =

struct with fields:

c_func: @applymattrc_fwd
ColorSpace_in: 'rgb'
ColorSpace_out: 'xyz'
encoding: 'double'
cdata: [1×1 struct]


mattrc = cform.cdata.sequence.source.cdata.MatTRC

mattrc =

struct with fields:

RedColorant: [0.4361 0.2225 0.0139]
GreenColorant: [0.3851 0.7169 0.0971]
BlueColorant: [0.1431 0.0606 0.7141]
RedTRC: [1024×1 uint16]
GreenTRC: [1024×1 uint16]
BlueTRC: [1024×1 uint16]



The "MatTRC" computation applies a "tone-response curve" to each of the input color components, and then it multiplies by a matrix formed from RedColorant, GreenColorant, and BlueColorant. Here are the tone response curves.

x = linspace(0,1,length(mattrc.RedTRC));
subplot(2,2,1)
plot(x,double(mattrc.RedTRC)/65535)
title('Red TRC')
subplot(2,2,2)
plot(x,double(mattrc.GreenTRC)/65535)
title('Green TRC')
subplot(2,2,3)
plot(x,double(mattrc.BlueTRC)/65535)
title('Blue TRC')


#### Step 2: Adjust the blackpoint.

This step is performed using the private function applyblackpoint and the parameters in the nested cdata struct.

cform.cdata.sequence.fix_black

ans =

struct with fields:

c_func: @applyblackpoint
ColorSpace_in: 'xyz'
ColorSpace_out: 'xyz'
encoding: 'double'
cdata: [1×1 struct]


cform.cdata.sequence.fix_black.cdata

ans =

struct with fields:

colorspace: 'xyz'
upconvert: 1



For these parameters, I can see from reading applyblackpoint that the specific computation performed is:

out = (0.0034731 * whitepoint) + (0.9965269 * in)

#### Step 3: Convert from CIEXYZ to CIELAB

Here are the specifics:

cform.cdata.sequence.fix_pcs

ans =

struct with fields:

c_func: @xyz2lab
ColorSpace_in: 'xyz'
ColorSpace_out: 'lab'
encoding: 'double'
cdata: [1×1 struct]


cform.cdata.sequence.fix_pcs.cdata

ans =

struct with fields:

whitepoint: [0.9642 1 0.8249]



The function that is used here, xyz2lab is not the documented, user-callable toolbox, but a private version of it. As with the other private functions, you can see it by typing:

edit private/xyz2lab

The whitepoint parameter for the computation is the standard whitepoint value used for ICC profile computations:

whitepoint('icc')

ans =

0.9642    1.0000    0.8249



#### Step 4: Convert from CIELAB to the destination space, SWOP CMYK

This turns out to the most complicated step. Here is the computational function handle and the parameters used:

cform.cdata.sequence.destination.c_func

ans =

function_handle with value:

@applyclut


cform.cdata.sequence.destination.cdata.luttag

ans =

struct with fields:

MFT: 4
PreShaper: {[256×1 uint16]  [256×1 uint16]  [256×1 uint16]}
PreMatrix: [3×4 double]
InputTables: {[256×1 uint16]  [256×1 uint16]  [256×1 uint16]}
CLUT: [21×21×21×4 uint16]
OutputTables: {1×4 cell}
PostMatrix: []
PostShaper: []



You really have to step through the execution of applyclut in the debugger to see each of about 6 substeps. In this case it turns out that only 2 of the substeps actually have an effect. One is a simple scaling to account for encoding differences between the two ICC profiles:

out = in * (257 / 256);

The second substep that matters is the big one, and it uses this 4-D lookup table:

size(cform.cdata.sequence.destination.cdata.luttag.CLUT)

ans =

21    21    21     4



This is really 4 different 3-D lookup tables packed into one array. The three dimensions of each lookup table correspond to the three components of the CIELAB space (L*, a*, and b*), and the 4 different tables correspond to the four components of the output space (C, M, Y, and K). The applyclut file contains a local function, clutinterp_tet3, that performs tetrahedral interpolation. See my 24-Nov-2006 blog post for a discussion of tetrahedral interpolation for color-space conversion.

All of the other steps do have relatively simple formulas associated with them, but this last step, based on these multidimensional lookup tables, does not.

Now you know, more or less, the computational steps for converting this orange color to SWOP CMYK.

orange_rgb = [215 136 37]/255;
clf
patch([0 1 1 0 0],[0 0 1 1 0],orange_rgb);

orange_cmyk = applycform(orange_rgb,cform)

orange_cmyk =

0.0286    0.4748    0.8917    0.1153



That's just a tiny bit of cyan, a fair amount of magenta, a lot of yellow, and small amount of black.

Published with MATLAB® R2018b

|