Updating libheif
Closed, ResolvedPublic

Description

We were using libheif... 1.4 or the like, and since then the library has updated quite a bit.

Let's go over the new features.

Color Profiles

Heif and av1f both have icc, restricted icc and nclx.

Restricted ICC

Restricted ICC profiles are a thing from jp2:

http://wiki.opf-labs.org/display/TR/Handling+of+ICC+profiles

https://www.bitsgalore.org/2013/07/01/icc-profiles-and-resolution-jp2-update-2011-d-lib-paper

https://www.dlib.org/dlib/may11/vanderknijff/05vanderknijff.html

As far as I can gather, lcms supports it:

https://sourceforge.net/p/lcms/mailman/lcms-user/thread/AANLkTinrH1DyqPGeYcmwBI3LMxLPaBJJIwKbMsiX-3mg%40mail.gmail.com/#msg25370985

It seems to be that the restricted profile is an attempt at simplifying icc profiles, but in turn just complicates everything. In practice, these are always matrix shaper profiles.

  • we should hunt down ABoxer and ask him if he knows anything.

Answer: Aaron Boxer suggested we'd ask https://github.com/openpreserve/jpylyzer for the usage in openjpeg. Spec wise, restricted profiles are monochrome or 3-component(rgb), matrix shaper profiles, and are of the display or input type. We can just load them with LCMS, for saving I guess we'll have to make a check against these characteristics first, and for jp2 either convert, and for heif use that to decide whether to store inside the ricc tag or the prof tag. As an aside, it seems openjpeg uses a special cmyk colorspace, but I'll need to figure out where it is defined and it is not relevant to this task.

NCLX

nclx is an ancient quicktime color space description that roughly corresponds to a matrix shaper profile:

https://poynton.ca/notes/misc/sde-nclc-vui-nclx.html

GIMP converts this to a icc profile, but I need to double check it does elle stone's corrections:

https://gitlab.gnome.org/GNOME/gimp/-/blob/master/plug-ins/common/file-heif.c#L528

Elle Stone's quantization corrections:

https://ninedegreesbelow.com/photography/well-behaved-profiles-quest.html#hexadecimal-quantization

  • Quantization is done.
  • Generate profiles if we cannot find matching profiles.
  • Work on moving the transfer functions that cannot be put into icc as conversions, and convert such heif files into 32 bit float.
  • Work on transfer function matching. -not possible-

C++ bindings

The c++ bindings haven't been updated for a while and misses the ability to get the color profile.

https://github.com/strukturag/libheif/commits/master/libheif/heif_cxx.h

  • We need c++ bindings first as everything in the current plugin uses these.
  • NCLX will proly need a nclx_to_icc kind of function in the lcms color engine.

Color Spaces

Heif supports YCrCb/YUV and Monochrome, and in theory RGB(A).

https://github.com/strukturag/libheif/issues/62

Av1f only supports YCrCb/YUV and Monochrome with seperate alpha channels.

Libheif takes care of the conversion to yuv if we pass it rgb data, but the conversion algorithm has the usual rounding problems. The problem is that the algorithm is also spec-defined so we can't try to make a better conversion here. The result is that crisp line art will have very strange looking anti-aliasing.

Also check https://jakearchibald.com/2020/avif-has-landed/

  • Let's figure out what the current state of this is. Seems to be 'resolved' by using an 'identity' matrix.
  • We also should try to make sure that end-users are made aware of this limitation as it makes 'lossless' not truly lossless
  • We probably should support monochrome. Right now we only support 8bit rgb

Bit-depth

The library right now supports 8, 10 and 12 bit. Krita and GIMP both only support 8 and 16 bits.

GIMP solves this by just scaling up the values for 10 and 12 bit.

I am unsure if this is correct, as I cannot tell if 10 and 12 bit heif/av1f images are always understood to be HDR images, especially as most of the time that also requires HDR metadata (maxfall/maxcll).

  • Figure out if 10 and 12 bit libheif images are always understood to be HDR Answer: it's profile dependant.
  • We really need a maxfall/maxcll function, see T10627 . (not gonna do right now)

Av1f support

Av1f support is possible if compiled with libaom. There's also rav1e support for 1.8.0, which is a rust library. The reason this is interesting is because libaom is a slow (unoptimized reference) encoder.

Outside of the RGB thing, most features seem to be the exact same between the two formats.

We will probably want to implement this support in the same way we have several fileformats use qimageio.

Mimetypes can be found here: https://github.com/strukturag/libheif/blob/master/libheif/heif.h#L294

  • Figure out just how sad sysadmin will be if we try to compile libheif with rav1e.
  • We don't seem to need extra cpp bindings, as what is necessary is to change the encoder for libheif depending on the mimetype, and there's already bindings for those

Amyspark is handling this.

Animation

libheif has support for multiple images in one file, in two different ways. One is an image sequence, the other is tiled images, which seems to be for panorama style images. The latter can only be read.

These images are in a sort of tree structure, with the image sequence images being top level, and the tiled images being sub-images of the top level ones.

Right now we only load the primary image. We could in theory load and save animation sequences using libheif. This would be great, because if av1f and heif are going to get the appropriate support on social media, then this would be a much newbie-friendlier way to handle small animation export than our ffmpeg situation. Given that we currently are seeing a couple of refactorings in animation, I would like to delay this until we get the blessing of @emmetoneill and @eoinoneill

EDIT: it seems that libheif, while supporting the saving of multiple images, doesn't support the video compression of these yet.

Wait until refactors are merged before we plan how to tackle this.
Also see: https://github.com/strukturag/libheif/issues/57 and https://github.com/strukturag/libheif/issues/129, seems we can load and save sequences but not necessarily timing data
Figure out if it is desirable or at all necessary to handle tiled image reading

Not gonna do.

Depth map

heif files support a depth image, which is a grayscale image representing a z-buffer, and is used for adding 3d effects to images.

Within image manipulation, the z-buffer is often used as a mask on a blur filter to create a depth-of-field effect. Exr files from blender can have the z-buffer as a render pass.

We could in theory support this by loading depth images as grayscale layers, and allowing the user to select a layer as a depth image, which will then get converted to grayscale and removed/hidden on the layerstack before we save the rest of the image.

This needs some overview with artists, but it seems like a workflow that makes sense...

Not gonna do.

Thumbnails

We can save thumbnails with libheif.

We don't do anything with thumbnail images ourselves, but we do of course have all the features to make thumbnail saving easy. I am currently not sure if libheif just generates a thumbnail for us.

  • Check if libheif generates thumbnail images for us.

Image transformations

So, because mobile phones do not want to transform the actual pixel data of an image for performance reasons, some images contain internal image transformations. Libheif supports loading an applying image transformations.

I am only bringing this up because of https://bugs.kde.org/show_bug.cgi?id=425832 which is about a similar thing, but then for tiff.

  • Figure out what we want to do with these

Libheif seems to apply them for us.

Overlay images

Libheif supports read overlay images... I have no idea what these are

  • Figure out what these are.

These are subtitles and the like, we're not going to tackle them until we see them in the wild.

woltherav created this task.Sep 1 2020, 3:36 PM
woltherav triaged this task as Wishlist priority.
woltherav updated the task description. (Show Details)Sep 1 2020, 3:41 PM
woltherav updated the task description. (Show Details)
woltherav updated the task description. (Show Details)Sep 1 2020, 3:53 PM
woltherav updated the task description. (Show Details)Sep 1 2020, 6:56 PM
mwein added a subscriber: mwein.EditedSep 2 2020, 1:42 AM

I guess it wouldn't hurt to poke Daniel_a_Simona if there's any news on this:
https://krita-artists.org/t/allow-import-export-of-images-in-av1-image-file-format-avif/4626/8

Libheif takes care of the conversion to yuv if we pass it rgb data, but the conversion algorithm has the usual rounding problems. The problem is that the algorithm is also spec-defined so we can't try to make a better conversion here. The result is that crisp line art will have very strange looking anti-aliasing.

Yes it's a bit sad AV1 does not support RGB while h265 does. For 8bpp there definitely will be some loss.
10/12bpp is a bit different, obviously you can't possibly store 3x16bits in less bits no matter the color space.

Question is how repeated YCbCr <=> RGB conversion behaves, at least unpacking 10/12bits to 16bits should make it possible to guarantee that any YCbCr value will be encoded back to the same value from RGB, on paper. In reality, no idea (see next text block...).
The lib at least allows to read the raw 10/12bit YCbCr data from what I can tell, but I also doubt we should try to do it better.

Not sure what you mean with "crisp line art will have very strange looking anti-aliasing" though, isn't that usually the side effect of chroma sub-sampling? I'd expect lossy color space conversion to show in subtle gradients, like if you convert an sRGB image to L*a*b* at 8bpp in Krita.

The library right now supports 8, 10 and 12 bit. Krita and GIMP both only support 8 and 16 bits.

GIMP solves this by just scaling up the values for 10 and 12 bit.

I am unsure if this is correct, as I cannot tell if 10 and 12 bit heif/av1f images are always understood to be HDR images, especially as most of the time that also requires HDR metadata (maxfall/maxcll).

If it's HDR or not...good question.
From what I understand, that entirely depends on how you specify the image content, either with an ICC profile, or by using a predefined value for primary colors and transfer characteristics.
Libheif defines a "heif_transfer_characteristics" type, with values like
heif_transfer_characteristic_ITU_R_BT_2100_0_PQ and ~ITU_R_BT_2100_0_HLG being HDR encoding standards, while ~IEC_61966_2_1 is the cryptic name for sRGB.

I'm not sure yet if/how libheif helps with converting all those possible values (11 primary color options and 17 transfer characteristics if I counted correctly) to something Krita understands if the image is not using an ICC profile we can just feed to LCMS.

If you want the full madness about the various ways to define a profile (including issues with consistent RGB <=> YUV conversions), maybe you have more luck understanding this:
https://github.com/AOMediaCodec/av1-avif/issues/84

those transfer characteristics are proly part of understanding the nlcx space description, and gimp just converts those to an icc profile.

That issue you linked is in particular because YUV calculations are supossed to use the XYZ-Y value of the colorants as coefficients. So if you want to go between YUV to RGB, you need to know the colorants of the space in question. I don't think it necessarily has to do with errors in the YUV conversion, more rather that one guy is like 'hey, what if we just always put the colorants in the file, then we don't need to rely on icc profiles having these, and will be able to use them for the YUV conversion.'

Not sure what you mean with "crisp line art will have very strange looking anti-aliasing" though, isn't that usually the side effect of chroma sub-sampling? I'd expect lossy color space conversion to show in subtle gradients, like if you convert an sRGB image to L*a*b* at 8bpp in Krita.

Yes, chroma subsampling usually does that, but while you and I know that, most users won't :) The thing is that we should just communicate to users that while heif and av1f say they can be lossless, the colorspace conversion means they should be treated the same way we treat jpeg: as lossy.

On sleeping on it, I may have overstated the scaling 10/12 to 16bit problem, scaling the values is fine, the problem is in particular, how do we know if a file needs to be interpreted as floating point, or as integer which in turn requires knowing if a file is HDR/Scene referred. We might indeed need to understand this from the associated colorspace or metadata.

mwein added a comment.Sep 2 2020, 9:17 PM

On sleeping on it, I may have overstated the scaling 10/12 to 16bit problem, scaling the values is fine, the problem is in particular, how do we know if a file needs to be interpreted as floating point, or as integer which in turn requires knowing if a file is HDR/Scene referred. We might indeed need to understand this from the associated colorspace or metadata.

Ah yes I almost forgot, while 16bit integer with say SMPTE ST 2084 (PQ) profile is technically HDR, it's not very suitable for editing (in krita, anyway), so editing such a file will usually be 10/12bit YUV => linear RGB "half" float => 10/12bit YUV.

So if there's an ICC profile it's easy to feed it to LCMS, but non-trivial to figure out if that's HDR or not, there's just so many ways to define a profile, especially with v4 profiles.

With the "CICP" and "nclx" stuff (is that essentially the same? Specs are all behind paywall, and in my experience are not exactly human readable anyway) it seems easy to divide the transfer characteristics into HDR and SDR.
But converting it into an ICC profile for LCMS... at first glance, that gimp code you linked doesn't even handle any of the HDR transfer characteristics, it just falls back to sRGB :(

woltherav added a comment.EditedSep 3 2020, 12:16 PM

Well, the thing about using a library is that it should handle most of the color space tag stuff for us if that is what is necessary to conform to the spec. The only thing I am concerned about is whether most of the contrast in the image is in the 0-1 range (a hdr image needing a floating point space) or the contrast is all over the place (16bit integer image).

I've made a github issue for the cpp bindings and also asked about the HDR stuff and emailed aaron boxer about restricted icc profiles.

woltherav updated the task description. (Show Details)Sep 10 2020, 6:06 PM
woltherav updated the task description. (Show Details)Sep 11 2020, 12:42 PM

Put a branch over here: https://invent.kde.org/graphics/krita/-/commits/wolthera/libheif-1-9-1-update

We can now load monochrome images as grayscale. Attaching a bunch of testfiles, also generated with libheif from pngs. The avif ones cannot be opened in any version of Krita yet, but they can be opened with chrome. What is up with the grayscale avif image, no idea.

mwein added a comment.Sep 30 2020, 3:20 AM

Hm libheif is confusing me, libheif.h claims:

If colorspace or chroma is set to heif_colorspace_undefined or heif_chroma_undefined, respectively, the original colorspace is taken.

Totally confused why I would get a 10-bit RGB image when AV1 doesn't even support RGB, I dug through the code to find out it unconditionally converts YCbCr to RGB in HeifContext::decode_image_planar():

heif_colorspace target_colorspace = (out_colorspace == heif_colorspace_undefined ?
                                      img->get_colorspace() :
                                      out_colorspace);

 if (target_colorspace == heif_colorspace_YCbCr) {
   target_colorspace = heif_colorspace_RGB;
 }

Bit depth is kept at 10/12 bits on the conversion which seems not ideal. While YCbCr generally needs more bits than RGB, only special matrix coefficients avoid float math/rounding, like YCbCo.
But from reading the code, when no NCLX is set explicitly, it creates an internal NCLX profile with "heif_matrix_coefficients_ITU_R_BT_601_6" for converting the input to YCbCr in preparation for HEVC/AV1 encoding, which means float calculations.

Yeah, and I think this is a case of the file format(s) being too new (they're basically the same format with h264/5 replaced by av1). I asked what was the best practice with the NCLX profile, and the libheif maintainer was like 'well, it's not necessary', while the original ticket that requested NCLX alongside ICC for avif did so because that would improve the quality of the YCrCb conversion a lot.

The whole situation is compounded by... I think there's no 10bit files in the nokia heif test list. Netflix does have 10bit files in their avif samples, but I haven't gotten to the point of making avif work. I guess we won't see what is wisest until we get it into a working state.

mwein added a comment.Oct 2 2020, 3:44 PM

Where are you stuck with AVIF loading?
I was at first clueless why adding the .avif extension to plugins/impex/heif/krita_heif_import.json didn't work, until I found with some grep-fu that I first need to add it to libs/koplugin/KisMimeDatabase.cpp.
I'm just not sure about the actual MIME types, apparently there are separate types, image/heic == HEIF+HEVC and image/avif == HEIF+AV1, so I added it as new type, and could get Krita to load them.

I think I've already mentioned that for >8-bits/channel I just reinterpret_cast<const uint16_t*>() the pointers returned by heifimage.get_plane() and divide the stride by 2 (since it's in Bytes and the code adds it as index offset).
10- and 12-bit images will then come out extremely dark because you need to rescale it to 16-bits, my quick and dirty solution was to just left-shift the read values by (16 - bit/channel).

Oh and I stumbled across another AVIF test image collection (think it was linked in some Firefox announcement):
https://resources.link-u.co.jp/avif/

Firefox 82 even started loading them as part of an HTML page after setting image.avif.enabled, although it obviously neither supports alpha channels, transformations, crop, or image sequences yet...

I am not stuck, I just got too preoccupied with other Krita tasks like release notes and the like, and I take the center of the week off (because I work in the weekends).

woltherav added a comment.EditedOct 2 2020, 8:21 PM

OK, we've got...

  • grayscale support for 8bit images.
  • boilerplate code for loading the icc profile if present... however, it seems that the cpp bindings for these are... broken. https://github.com/strukturag/libheif/issues/353
  • ugly code for loading 10 and 12 bit images.
  • enabled avif mimetypes, libheif seems to load these.

todo for these items only:

  • make code less ugly.
  • handle alpha channels.
  • hope for bug to be handled...
  • make sure that Krita understands whether or not the library is built with avif support.

Today...

  • enabled avif support for the encoder/export.
  • allowed for saving of grayscale 8bit images.
  • allowed for saving of 16bit rgba images.
  • allowed for saving the color profile.

Need to check if I am right about encoding 16bit monochrome images, but this is a pretty good haul, I think :<

Didn't do much today, as most of my day was taken up by the multithreading seminar. I did however find that there's plenty of avif test-images on the avif repository: https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles

From there, I managed to get a Netflix hdr image loaded. It just has a bt2100 nclx profile attached.

woltherav updated the task description. (Show Details)Oct 8 2020, 8:58 PM
woltherav added a comment.EditedOct 10 2020, 7:06 PM
  • made sure that colorspaces get converted properly
  • disabled the 16bit grayscale usecase as I can't seem to get it to load
  • cleanup up the importer for high bit depths.
  • made sure we load alpha properly.
  • did a poc of loading nclx... except libheif still doesn't hand me color profile info and no movement on that bug is done.

Made an MR:

https://invent.kde.org/graphics/krita/-/merge_requests/530

woltherav updated the task description. (Show Details)Feb 23 2021, 6:22 PM
woltherav updated the task description. (Show Details)Feb 24 2021, 8:49 PM
woltherav updated the task description. (Show Details)Mar 4 2021, 6:11 PM
woltherav closed this task as Resolved.Mar 23 2021, 1:30 PM

Going to resolve this now, considering it's merged. For a next itteration, we should just make a new task.