Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resizing may result in degraded quality because gamma is not compensated #1604

Closed
znerol opened this issue Dec 21, 2015 · 8 comments
Closed

Comments

@znerol
Copy link

znerol commented Dec 21, 2015

Citing from http://www.4p8.com/eric.brasseur/gamma.html

Technically speaking, the problem is that "the computations are performed as if the scale of brightnesses was linear while in fact it is a power scale." In mathematical terms: "a gamma of 1.0 is assumed while it is 2.2." Lots of filters, plug-ins and scripts make the same error.

Pillow seems to be affected by that problem too as the following test script demonstrates:

from PIL import Image
from cStringIO import StringIO
from urllib2 import urlopen

f = urlopen('http://www.4p8.com/eric.brasseur/gamma_dalai_lama_gray.jpg')
img = Image.open(StringIO(f.read()))
img.show()

newsize = tuple([i/2 for i in img.size])
imgscaled = img.resize(newsize, Image.ANTIALIAS)
imgscaled.show()
@znerol
Copy link
Author

znerol commented Dec 21, 2015

There is an SO question providing a workaround using numpy.

@wiredfool
Copy link
Member

There is essentially no color space awareness in Pillow by default. It's possible to have some compiled in, but it's optional. So it's not terribly surprising that it's not taking one specific colorspace into account when doing geometric transformations.

If you really wish to do it properly, you're probably better off doing the operations in L_a_b space instead of sRGB anyway.

@znerol
Copy link
Author

znerol commented Dec 22, 2015

Right. For future reference, this is one way to implement transformations in linearized RGB color space:

from PIL import Image, ImageCms
from cStringIO import StringIO
from urllib2 import urlopen

f = urlopen('http://www.4p8.com/eric.brasseur/gamma_dalai_lama_gray.jpg')
#f = urlopen('http://www.4p8.com/eric.brasseur/gamma_cloud_gate.jpg')
img = Image.open(StringIO(f.read()))
img.show()

newsize = tuple([i/2 for i in img.size])

# Load profiles.
srgb_profile = ImageCms.getOpenProfile("/usr/share/color/icc/sRGB.icc")
lsrgb_profile = ImageCms.getOpenProfile("linearized-sRGB.icc")

# Linearize RGB color space, resize and convert back to sRGB.
if not img.mode in ('RGB', 'RGBA'):
    img = img.convert('RGB')

lsrgbimg = ImageCms.profileToProfile(img, srgb_profile, lsrgb_profile)
lsrgbscaled = lsrgbimg.resize(newsize, Image.ANTIALIAS)
srgbscaled = ImageCms.profileToProfile(lsrgbscaled, lsrgb_profile, srgb_profile)

srgbscaled.show()

Note, that linearized-sRGB.icc can be produced using Photoshop color settings (custom RGB with a gamma of 1.0). Also note that this does not work for dark images unless the channel depth is at least 16 bit per pixel (uncomment the second urlopen call to see the effect). I do not think that Pillow supports something different than 3x8 bit RGB at the moment.

@wiredfool thanks for the quick response and the pointers.

@znerol
Copy link
Author

znerol commented Dec 22, 2015

linearized-sRGB.icc.zip

@wiredfool
Copy link
Member

Colorspace aware transforms might be something that's appropriate for inclusion with the ImageCMS portion. (I assume that we'd be able to either include or generate a linear sRGB profile, and that there wouldn't be copyright issues like we've had with some of the other profiles)

The one tricky bit is that you'd really want to do one conversion into linear space, and then do all of the transforms, and then transform out. Otherwise there would likely be some pretty horrible precision losses for repeated operations, especially given that we're limited to 8 bits per channel for color images.

@znerol
Copy link
Author

znerol commented Dec 30, 2015

.specially given that we're limited to 8 bits per channel for color images.

What is the reason for this limitation?

@wiredfool
Copy link
Member

There's no support anywhere in the C layer for more than 32 bits per pixel, nor more than 8 bits per channel.

It could be added, but it would be a large job and so far, no one has needed it enough to do it.

@baryluk
Copy link

baryluk commented Jan 9, 2020

This is extremely unfortunate. Lack of native sRGB support or 16-bit integer / 32-bit float per channel is a deal breaker. It breaks almost all transforms. Resizing, rotations, provided and custom ops, line drawing with alpha, filters, blurs, ImageMath.eval with +, -, *, /, drawing with anti-aliasing using aggdraw, it even breaks conversions between color spaces and ICC profiles due to significant quantization errors.

The provided 'workaround' is not actually working as it should, because it forces you to work in linear color space (which will result in very poor color quality in darker regions), but that is not really a good idea when using 8-bit per channel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants