-
-
Notifications
You must be signed in to change notification settings - Fork 471
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
[WIP] Color management using ICC profiles #1446
base: main
Are you sure you want to change the base?
Conversation
Hello @hackermd! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:
Comment last updated at 2021-07-22 00:35:16 UTC |
Note that this feature requires Pillow, which is an optional dependency. |
Codecov Report
@@ Coverage Diff @@
## master #1446 +/- ##
==========================================
- Coverage 97.42% 97.38% -0.04%
==========================================
Files 66 67 +1
Lines 10185 10225 +40
==========================================
+ Hits 9923 9958 +35
- Misses 262 267 +5
Continue to review full report at Codecov.
|
intent = ImageCms.INTENT_RELATIVE_COLORIMETRIC | ||
if not isIntentSupported( | ||
profile, | ||
intent=intent, | ||
direction=ImageCms.DIRECTION_INPUT | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know almost nothing about ICC profiles, but is relative colorimetric correct? Standard has:
The following constraints on the encoding of the ICC Profile are recommended:
The Rendering Intent should be Perceptual.
And that's only a recommendation, not a requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also not sure what intent would be best suited for the library. We may want to add a parameter so that the caller can change the behavior if necessary. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dclunie do you have any thoughts on this?
Does this need to be a class in a new module? I was thinking of adding it as a function to the pixel utilities instead: def apply_icc_profile(
arr: "np.ndarray",
icc_profile: Optional[bytes] = None,
transform: Optional["PIL.ImageCms.CmsTransform"] = None
) -> "np.ndarray":
"""Return `arr` with an applied ICC profile.
.. versionadded:: 2.3
.. warning::
``apply_icc_profile()`` requires `Pillow
<https://pillow.readthedocs.io/en/stable/>`_ and
`NumPy<https://numpy.org/>`_
Parameters
----------
arr : numpy.ndarray
An :class:`~numpy.ndarray` containing single or multi-framed RGB pixel
data.
icc_profile : bytes, optional
The ICC profile to apply to `arr`, as given by (0028,2000) *ICC
Profile*. Required if `cms_profile` is not supplied. Only sRGB is
supported as an output color space.
transform : PIL.ImageCms.CmsTransform, optional
An existing transform to apply to `arr` rather than creating one
from `icc_profile`. Required if `icc_profile` is not supplied.
"""
if not HAVE_PIL or not HAVE_NP:
raise ImportError("apply_icc_profile() requires Pillow and NumPy")
if icc_profile is None and transform is None:
raise ValueError(
"One of 'icc_profile' or 'transform' is required"
)
if icc_profile and transform:
raise ValueError(
"Only one of 'icc_profile' or 'transform' may be used"
)
if arr.ndim not in (3, 4) or arr.shape[-1] != 3:
raise ValueError(
"'arr' has incorrect dimensions for RGB pixel data"
)
if icc_profile:
transform = ImageCms.buildTransform(
inputProfile=ImageCms.ImageCmsProfile(BytesIO(icc_profile)),
outputProfile=ImageCms.createProfile("sRGB"),
inMode="RGB", # according to PS3.3 C.11.15.1.1
outMode="RGB",
)
if arr.ndim == 3:
im = Image.fromarray(arr)
ImageCms.applyTransform(im, transform, inPlace=True)
return np.asarray(im)
for idx, frame in enumerate(arr):
im = Image.fromarray(arr)
ImageCms.applyTransform(im, transform, inPlace=True)
arr[idx] = np.asarray(im)
return arr |
) | ||
return ImageCms.buildTransform( | ||
inputProfile=profile, | ||
outputProfile=ImageCms.createProfile('sRGB'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a note somewhere that sRGB is the only supported color space
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sRGB is a very limited color space and its use as an intermediary may result in out of gamut colors being inappropriately handled - how is your color management implementation going to take advantage of a calibrated monitor (especially a "wide gamut" monitor), or in any case, with a gamut wider than sRGB supports (not to mention the quantization issues caused by using 8 bit per channel intermediary spaces for transformations that are not linear)?
I appreciate that anything beyond an sRGB working space in HTML5 Canvas may be a bit beyond the state of the art at the moment, but if this is not handled properly, images in browsers may display poorly compared to thick clients; see also https://lists.w3.org/Archives/Public/public-colorweb/2021Dec/att-0002/2021-Conversion_Overview.pdf.
You might want to consult with some color experts from ICC, W3C and FDA.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your feedback @dclunie. For the Python library, my focus has been on image analysis (for which sRGB may be ok?) rather than display use cases. However, I agree that we should implement this in a way that would work for a variety of use cases.
@scaramallion we could just add an optional parameter output_profile
to allow the caller to provide the output ICC profile as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the caller can already provide the transform object, we may indeed just need to document the default behaviour.
I would prefer a "functor" instead of a function. For multi-frame images with a large number of frames, we don't want to build the transform object over and over again for every frame. It would be nice to just create an instance of a class and repeatedly use that instance. This function allows for that, too, since the transform object can be passed as an argument. However, it thereby exposes implementation details (Pillow type), which I was trying to avoid. |
I did include a way to re-use the transform for a multi-frame array (apart from supplying the I don't think I'm too fussed about the "PIL.ImageCms.CmsTransform" 🤷 |
I would personally prefer: class ColorRenderingIntent(enum):
PERCEPTUAL = 'perceptual'
RELATIVE_COLORIMETRIC = 'relative_colorimetric'
ABSOLUTE_COLORIMETRIC = 'absolute_colorimetric'
class ColorTransformer:
def __init__(
self,
icc_input_profile: bytes,
icc_output_profile: Optional[bytes] = None,
rendering_intent: ColorRenderingIntent = ColorRenderingIntent.PERCEPTUAL
):
pass
def __call__(self, image: numpy.ndarray) -> numpy.ndarray:
pass color_transform = ColorTransformer(icc_input_profile)
for frame in selected_frames:
corrected_frame = color_transform(frame) Since Pillow is an optional dependency, I would avoid exposing the Pillow type via the public API. This will allow us to change the implementation if necessary without breaking backwards compatibility. The callable "functor" style is also widely used by other Python libaries (e.g., torchvision.transforms). |
Describe the changes
Adds support for color management using ICC Profiles (see #1244).
Tasks
doc/_build/html/index.html
)