Colour management with ImageCms

Pillow · Expert · 12 min

Without colour management, wide-gamut phone photos and print CMYK files look dull or shifted after processing.

The same RGB numbers mean different colours in different colour spaces. Most of the web assumes sRGB, but iPhones shoot in Display P3, photographers use Adobe RGB, and print uses CMYK with device-specific profiles. The space is described by an ICC profile embedded in the file, found in im.info.get("icc_profile").

Pillow's convert("RGB") on a CMYK image, or simply ignoring the profile, does a naive conversion. Results look wrong: CMYK JPEGs come out oversaturated or muddy, and P3 photos look washed out once the profile is lost.

`ImageCms` (a wrapper around LittleCMS) does proper conversions:

  • ImageCms.ImageCmsProfile(io.BytesIO(icc_bytes)) loads the embedded profile
  • ImageCms.createProfile("sRGB") creates a standard target
  • ImageCms.profileToProfile(im, src, dst, outputMode="RGB") converts pixels
  • buildTransform(...) once plus applyTransform(im, t) per image is faster for batches

The rendering intent decides how out-of-gamut colours are handled: perceptual compresses the whole gamut smoothly, relative colorimetric clips only what doesn't fit.

A robust pipeline for web output: if the image has a profile, convert it to sRGB with ImageCms; then either embed an sRGB profile with save(..., icc_profile=ImageCms.ImageCmsProfile(srgb).tobytes()) or omit it, since browsers assume sRGB.

Example

import io
from PIL import Image, ImageCms

srgb = ImageCms.createProfile("sRGB")
lab = ImageCms.createProfile("LAB")

im = Image.new("RGB", (2, 1), (255, 0, 0))
to_lab = ImageCms.buildTransform(srgb, lab, "RGB", "LAB")
lab_im = ImageCms.applyTransform(im, to_lab)
print(lab_im.mode, lab_im.getpixel((0, 0))[0] > 100)   # LAB True (L of red ~54%)

icc = ImageCms.ImageCmsProfile(srgb).tobytes()        # embed a profile
im.save("tagged.png", icc_profile=icc)
src = Image.open("tagged.png")
prof = ImageCms.ImageCmsProfile(io.BytesIO(src.info["icc_profile"]))
print(ImageCms.getProfileDescription(prof).strip())   # sRGB built-in (or similar)

out = ImageCms.profileToProfile(src, prof, srgb, outputMode="RGB",
                                renderingIntent=ImageCms.Intent.PERCEPTUAL)
print(out.getpixel((0, 0)))                            # (255, 0, 0) same space

Remember

  • ICC profiles in im.info define what RGB numbers mean
  • Use ImageCms.profileToProfile or build/applyTransform to convert
  • Convert to sRGB for the web; keep or embed the profile on save
  • Rendering intent controls out-of-gamut handling

Watch out for

  • Converting CMYK print files to RGB with plain convert() and getting badly shifted colours.

Check yourself

  1. A Display P3 photo is re-saved without its ICC profile. How does it look in a browser?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pillow.readthedocs.io/en/stable/reference/ImageCms.html

Builds on

Unlocks

Nothing builds on this yet.