蛍光グリーンは印刷で黄緑に沈む。sRGBの12色をCMYKへ往復させて測った

蛍光グリーンは印刷で黄緑に沈む。sRGBの12色をCMYKへ往復させて測った

純粋な蛍光グリーンの #00FF00 を、印刷用のCMYKへ直して画面へ戻すと #6CB537 になります。RGBで書けば (108, 181, 55)。落ち着いた黄緑です。

ところが、最初に書いた検査は12色すべてに「変化なし」と答えました。この食い違いが今回の記録です。

ここで使った12色は、画像生成AIの出力ではありません。選んだ合成見本です。買い手から色が違うと言われた話でもありません。紙にも刷っていません。画面の中で、sRGBからCMYKの印刷条件へ往復させて測った検証メモです。

売るのは画面の色、届くのは紙の色

ポスターやステッカー、カレンダーのデータを売る場面を考えます。データは画面で作るので、色はsRGBです。印刷はインクの色、つまりCMYKで行います。買った人が印刷所へ出すどこかで、sRGBからCMYKへの変換が入ります。

変換先に入りきらない色は、近い色へ寄せられます。どれくらい寄るのか。その数字を、この回の前には持っていませんでした。商品ページに「印刷では色が変わります」と書くにしても、何色がどのくらい変わるのか分からなければ、ただの定型文です。

最初の一行は「異常なし」と答えた

最初はImageMagickの一行で済むと考えました。CMYKへ変換して、sRGBへ戻し、元の色と比べます。

mkdir -p "$TMPDIR/cmyk" && cd "$TMPDIR/cmyk"
cat > swatches.txt <<'EOF'
00FF00 純緑
0000FF 純青
FF0000 純赤
00E5FF ネオンシアン
FF2D95 蛍光ピンク
7CFC00 ライム
1F51FF 電気青
FF6A00 オレンジ
E8B79A 肌色
808080 中間グレー
14213D 濃紺
FFFFFF 白
EOF
cat > naive.sh <<'EOF'
#!/bin/sh
while read -r hex name; do
  back=$(magick -size 1x1 "xc:#$hex" -colorspace CMYK -colorspace sRGB -depth 8 -format '%[hex:p{0,0}]' info:)
  echo "$hex $name -> $back"
done < swatches.txt
EOF
chmod +x naive.sh && ./naive.sh
00FF00 純緑 -> 00FF00
0000FF 純青 -> 0000FF
FF0000 純赤 -> FF0000
00E5FF ネオンシアン -> 00E5FF
FF2D95 蛍光ピンク -> FF2D95
7CFC00 ライム -> 7CFC00
1F51FF 電気青 -> 1F51FF
FF6A00 オレンジ -> FF6A00
E8B79A 肌色 -> E8B79A
808080 中間グレー -> 808080
14213D 濃紺 -> 14213D
FFFFFF 白 -> FFFFFF

12色すべて、1ビットも動きませんでした。CMYKの値を見ると、#00FF00 は C100% M0% Y100% K0% です。

magick -size 1x1 xc:'#00FF00' -colorspace CMYK -format '%[pixel:p{0,0}]\n' info:
cmyk(255,0,255,0)

この結果から「印刷でも問題なし」と読むのは誤りです。プロファイルなしの変換は、インクの色や紙の情報を持たない換算です。条件が空のまま往復しても、差は出ません。検査として何も測っていませんでした。

印刷条件のプロファイルを通して測り直す

必要なのは、印刷条件を記録したICCプロファイルです。手元のDebianには colord-data パッケージ経由で、FOGRA39L Coated が入っていました。ヨーロッパの塗工紙向けの標準的な条件です。日本の印刷所が指定しやすい Japan Color 2001 は、この環境に見当たりませんでした。近い条件で代えた測定、という位置づけです。

Pillow の ImageCms で、sRGBからCMYKへ、CMYKからsRGBへと2回変換します。元の色と戻った色の差は、CIELAB空間の距離(ΔE76)で出します。

icc_check.py の全文(38行)
import math, sys
from PIL import Image, ImageCms

def to_lab(rgb):
    def lin(v):
        v /= 255
        return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
    r, g, b = (lin(v) for v in rgb)
    x = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047
    y = 0.2126 * r + 0.7152 * g + 0.0722 * b
    z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883
    f = lambda t: t ** (1 / 3) if t > 216 / 24389 else (24389 / 27 * t + 16) / 116
    fx, fy, fz = f(x), f(y), f(z)
    return 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)

def main(profile_path, swatch_path):
    cmyk = ImageCms.getOpenProfile(profile_path)
    if cmyk.profile.xcolor_space.strip() != "CMYK":
        sys.exit(f"CMYKプロファイルではありません: {profile_path}")
    rows = [line.split(maxsplit=1) for line in open(swatch_path, encoding="utf-8") if line.strip()]
    if not rows:
        sys.exit(f"見本が0行です: {swatch_path}")
    srgb = ImageCms.createProfile("sRGB")
    kw = dict(renderingIntent=ImageCms.Intent.RELATIVE_COLORIMETRIC, flags=ImageCms.Flags.BLACKPOINTCOMPENSATION)
    fwd = ImageCms.buildTransform(srgb, cmyk, "RGB", "CMYK", **kw)
    rev = ImageCms.buildTransform(cmyk, srgb, "CMYK", "RGB", **kw)
    far = 0
    for hexv, name in rows:
        rgb = tuple(int(hexv[i:i + 2], 16) for i in (0, 2, 4))
        c = ImageCms.applyTransform(Image.new("RGB", (1, 1), rgb), fwd)
        back = ImageCms.applyTransform(c, rev).getpixel((0, 0))
        de = math.dist(to_lab(rgb), to_lab(back))
        far += de > 10
        cmyk_pct = "/".join(f"{round(v * 100 / 255):3d}" for v in c.getpixel((0, 0)))
        print(f"{hexv} CMYK% {cmyk_pct}  戻り {back[0]:3d},{back[1]:3d},{back[2]:3d}  dE76 {de:5.1f}  {name.strip()}")
    print(f"dE76が10を超えた見本: {far} / {len(rows)}")

main(*sys.argv[1:3])

実行します。swatches.txt は前の手順で作ったものです。

python3 icc_check.py /usr/share/color/icc/colord/FOGRA39L_coated.icc swatches.txt
00FF00 CMYK%  63/  0/ 96/  0  戻り 108,181, 55  dE76  55.1  純緑
0000FF CMYK%  98/ 74/  0/  0  戻り  13, 76,155  dE76  89.1  純青
FF0000 CMYK%   0/ 96/ 98/  0  戻り 229, 28, 24  dE76  15.8  純赤
00E5FF CMYK%  53/  0/ 13/  0  戻り 124,204,223  dE76  19.7  ネオンシアン
FF2D95 CMYK%   1/ 91/  5/  0  戻り 230, 45,133  dE76   9.1  蛍光ピンク
7CFC00 CMYK%  53/  0/ 95/  0  戻り 142,191, 51  dE76  43.0  ライム
1F51FF CMYK%  87/ 60/  0/  0  戻り  36, 98,172  dE76  60.9  電気青
FF6A00 CMYK%   0/ 69/ 97/  0  戻り 237,106, 20  dE76   9.8  オレンジ
E8B79A CMYK%   9/ 33/ 40/  0  戻り 232,183,154  dE76   0.0  肌色
808080 CMYK%  50/ 41/ 40/ 18  戻り 127,128,128  dE76   0.4  中間グレー
14213D CMYK%  87/ 62/ 10/ 76  戻り  20, 35, 62  dE76   1.4  濃紺
FFFFFF CMYK%   0/  0/  0/  0  戻り 255,255,255  dE76   0.0  白
dE76が10を超えた見本: 6 / 12

さっきは全部0だった差が、一番小さい白と肌色で0.0、一番大きい純青で89.1になりました。純緑の戻り値 (108, 181, 55) が、冒頭の #6CB537 です。

10という線引きは仮に置いた

「10を超えたら別の色」という基準は、規格ではありません。ΔE76の数字を見ながら、画面で並べたときに別の色と言いたくなる境目を、この見本用に置きました。根拠は薄いです。

その線で見ると、きれいに2つに分かれます。純青、電気青、純緑、ライムは40から89で、ネオンシアンと純赤も16から20に入ります。肌色、中間グレー、濃紺、白は1.4以下です。

蛍光ピンク(9.1)とオレンジ(9.8)は、線のすぐ下に並びました。10未満だから安全、とは言いません。線を8に引けば、この2色も入ります。ΔE76は人の見え方に合わせた後継の式(ΔE2000など)より粗い物差しなので、線の近くの2色は参考程度に見ています。

肌色やグレーが動かない一方で、青と緑が大きく動きます。この12色の範囲で言えるのは、中間色は印刷でもほぼ保たれ、原色に近い色ほど沈むということです。絵柄ごとの話は、12色だけでは判断できません。

ImageMagickのプロファイル指定と突き合わせる

Pillowの結果が正しいかを、別の道具で確かめます。同じプロファイルをImageMagickに渡し、同じ往復をさせます。

cat > magick_check.sh <<'EOF'
#!/bin/sh
P=/usr/share/color/icc/colord/FOGRA39L_coated.icc
S=/usr/share/color/icc/colord/sRGB.icc
while read -r hex name; do
  rgb=$(magick -size 1x1 "xc:#$hex" -intent Relative -black-point-compensation \
    -profile $S -profile $P -profile $S -depth 8 \
    -format '%[fx:int(255*p{0,0}.r+0.5)] %[fx:int(255*p{0,0}.g+0.5)] %[fx:int(255*p{0,0}.b+0.5)]' info:)
  echo "$hex $rgb"
done < swatches.txt
EOF
chmod +x magick_check.sh && ./magick_check.sh
00FF00 108 180 55
0000FF 10 75 155
FF0000 228 31 24
00E5FF 124 203 222
FF2D95 229 45 132
7CFC00 142 190 50
1F51FF 37 98 172
FF6A00 237 105 22
E8B79A 232 182 153
808080 127 127 127
14213D 18 33 61
FFFFFF 254 254 254

上の「戻り」の列と並べると、各成分の差は最大で3でした。青の赤成分が13と10で、これが最大です。変換エンジンの丸めの違いと考えていて、順位は入れ替わりません。

踏んだところ

TIFFに書いて読み直す道は、うまくいきませんでした。ImageMagickでCMYKのTIFFへ変換してから、色を読み直した結果です。

magick g.png -profile /usr/share/color/icc/colord/sRGB.icc -profile /usr/share/color/icc/colord/FOGRA39L_coated.icc g_fogra.tif
magick g_fogra.tif -format '%[pixel:p{0,0}]\n' info:
cmyk(0,0,0,0)

g.png は #00FF00 の1×1画素です。上の表では C63% Y96% のはずが、全部0と読めました。Pillowで同じファイルを開くと UnidentifiedImageError: cannot identify image file で止まります。1×1画素のTIFFだから起きたのか、書き出しの設定なのか、原因は追っていません。以降はファイルを挟まず、メモリ上で往復する形に切り替えました。

検査側の穴も塞ぎました。プロファイルにCMYKでないものを渡しても、黙って数字が出てしまうのは危険です。

python3 icc_check.py /usr/share/color/icc/colord/sRGB.icc swatches.txt; echo "exit=$?"
: > empty.txt
python3 icc_check.py /usr/share/color/icc/colord/FOGRA39L_coated.icc empty.txt; echo "exit=$?"
CMYKプロファイルではありません: /usr/share/color/icc/colord/sRGB.icc
exit=1
見本が0行です: empty.txt
exit=1

存在しないプロファイルのパスを渡したときは、Pillowが cannot open profile file で例外を出して終了コード1になります。どの入力でも、0円のような空の成功は返しません。

売るデータにどう使うか

まだ実際の商品には反映していません。この12色から言えるのは、次の2点です。

  • 蛍光やネオン系の色を使う印刷用データは、商品ページに「画面より沈む」と書くだけでは足りません。純緑なら (108, 181, 55) のように、戻り色の見本を添えたほうが買い手に伝わります。
  • 配色を決める段階で、元の色でなく戻り色を使います。印刷でくすむと分かっている色を、画面でだけ鮮やかに見せても意味が薄いからです。

納得していない点は3つあります。1つはΔE76の粗さです。2つ目はプロファイルで、日本の印刷所が実際に使う条件では数字が変わるはずです。3つ目は、紙そのものを測っていないことです。次は印刷所の指定プロファイルを手に入れて、同じ12色をもう一度流します。

用語

用語この記事での意味
sRGB画面表示用の標準的な色の表し方。画像生成AIの出力や、一般的な画像ファイルはこれで扱われる
CMYKシアン、マゼンタ、イエロー、黒のインクで色を作る方式。印刷用
ICCプロファイルある機器や印刷条件で、数値がどの色に見えるかを記録したファイル
色域その方式で出せる色の範囲。sRGBの鮮やかな緑や青は、CMYKの色域に入りきらない
ΔE76CIELAB空間での2色の距離。大きいほど違う色に見える。この記事では往復の前後で測った
レンダリングインテント色域に入らない色をどう寄せるかの方針。今回は相対的色域を使った
黒点補正暗い側の黒の深さを、変換先に合わせて揃える設定

出典

この記事をシェア