MrLixm / AgXc

Fork of Troy.S AgX, a display rendering transform available via OCIO and more
92 stars 8 forks source link

[OCIO] Pure black (0.0,0.0,0.0) and near values get "lifted", compared to original main-AgX (ociov2) version. #9

Closed jkierbel closed 1 year ago

jkierbel commented 1 year ago

Hi, I have been trying this config looking for a v1 compatible version of AgX. Troubleshooting it with Troy we found some interesting inconsistencies between this version and his original OCIOv2 one.

Here are some examples (made with this test image):

Image viewed with Troy's original main AgX (OCIO v2) image

Image viewed with this v1 compatible fork: image

Delta/difference pass between both: image

Same diff pass as above, but gain-boosted for clarity: image

Here is the analisys of a single pixel that is pure 0.0, 0.0, 0.0 on the original file, and remains like that with Troy's original config (bottom read), but gets lifted to 0.06219 on this fork (top read) image

So it seems the changes in appearance are the strongest at pure black values and fade out from near there to higher values. In grading, a simple "offset" doesn't fix it. A black-point re-map seems to get quite close to a perfect match, but non-uniformly (matching an area of the image gets another area further away, and so on). So it seems like a nuanced change on the "toe" of it? He suspects is something about the allocation variables or MatrixTransform.

I understand there is a disclaimer on the fork description about them not being 100% garanteed in accuracy, but maybe this is a useful insight, since that "lift" is kinda weird.

Cheers!

MrLixm commented 1 year ago

Hello, thanks for reporting. For sure not intended and need to be addressed. The 2 <AllocationTransform> transform in the AgX Log colorspace are the strongest suspect right now indeed.

I have a look and answer there on what I find. Cheers.

MrLixm commented 1 year ago

Also could you mention which software was used to produce the image please ?

jkierbel commented 1 year ago

I did them with Nuke since it supports both OCIO v2 and v1. I first noticed it by trying in on Affinity Photo 2 (that only supports v1, and it was to use it there that I was looking for v1 compatible version). After trying it there I went to Nuke for a sanity check on the appearance compared to the original config by Troy and noticed the discrepancy (which I then tested and remained the same when using this fork in Nuke, so it wasn't Affinity at fault, which was my first guess).

MrLixm commented 1 year ago

Ok found the issue.

Comes from the latest AllocationTransform : https://github.com/MrLixm/AgXc/blob/29653e921930b2cbf37835fac4958b43c9dbdac2/ocio/config.ocio#L83

I added an offset (the 0.0056065625) to the transform, which was not there in the original version. This was added upon testing on plate data :

Linear sRGB -> AgX Log (Kraken) (no offset)

image

Linear sRGB -> AgX Log (Kraken) (with offset)

image

Those are initially negative values or pure black values.

Also added because of the explanation in the documentation :

https://opencolorio.org/old/configurations/allocation_vars.html#allocationvars

one downside of this approach is that it can’t represent 0.0, which is why we optionally allow a 3d allocation var, a black point offset.

But of course, I don't revert this offset after so yeah it looks wrong.

What to do

sobotka commented 1 year ago

The lack of clipping is challenging in V1. I know that a basic uniform LUT will properly clip. Even a two entry one will work if memory serves. I was unable to diagnose any other tricks to get a clip / clamp to work.

MrLixm commented 1 year ago

I guess will go down that road then, seems other usual configs also use LUTs for log transforms.

MrLixm commented 1 year ago

First solution attempt :

  - !<ColorSpace>
    name: AgX Log (Kraken)
    family: AgX
    equalitygroup: ""
    bitdepth: 32f
    description: AgX Log (Kraken)
    isdata: false
    allocation: uniform
    allocationvars: [ -12.47393, 4.026069 ]
    from_reference: !<GroupTransform>
      children:
        # the 3 bottom transforms are used only as a hack to clamp negatives
        - !<AllocationTransform> { allocation: lg2, vars: [ -10, 7, 0.0056065625 ] }
        - !<FileTransform> { src: dummy.spi1d, interpolation: linear }
        - !<AllocationTransform> { allocation: lg2, vars: [ -10, 7, 0.0056065625 ], direction: inverse }
        - !<MatrixTransform> { matrix: [ 0.842479062253094, 0.0784335999999992, 0.0792237451477643, 0, 0.0423282422610123, 0.878468636469772, 0.0791661274605434, 0, 0.0423756549057051, 0.0784336, 0.879142973793104, 0, 0, 0, 0, 1 ] }
        - !<AllocationTransform> { allocation: lg2, vars: [ -12.47393, 4.026069] }
        - !<FileTransform> { src: dummy.spi1d, interpolation: linear }

Just reusing the same dummy lut I used pre-log to clamp post-log.

Seems to work fine but I still have a small difference with the default AgX implementation. I keep investigating.

MrLixm commented 1 year ago

Hey again, more experimentations, just found out my dummy lut solution to clip negative doesn't work (only the pre-log).

Results

test_log reference preview ↳ img0 reference image

test_log reference negatives ↳ img1 reference image - negatives mask

test_log default_agx ↳ img2 original AgX Log

test_log default_agx negatives ↳ img3 original AgX Log - negatives mask

test_log agxc_only_clip negatives ↳ img4 AgXc Log, with only the 3 transform for the clip

Analysis

First we can see that the original AgX Log (img3) introduce a lot of negatives on all the pure black pixels. For the initial negatives values, some are clamped properly, but other are still there after the transform. The interesting details is that all negatives have the same values -6.88037. I can't affirm if this is intentional to have negatives like that after a log transform but this looks wrong to me.

pre-clip

- !<AllocationTransform> { allocation: lg2, vars: [ -10, 7, 0.0056065625 ] }
- !<FileTransform> { src: dummy.spi1d, interpolation: linear }
- !<AllocationTransform> { allocation: lg2, vars: [ -10, 7, 0.0056065625 ], direction: inverse }

Then on img4 you can cleary see that my negative clip hack doesn't works. I got the same issue where pure blacks are turned into negative but here I keep all the original negatives, and even more weird, some of the original negatives are not negatives anymore (see tall bar at the middle right)

post-clip

[...]
 - !<FileTransform> { src: dummy.spi1d, interpolation: linear }

For my "post-clip", no picture was posted but because it is successful. It indeed clamp everything between 0-1 so I don't have any negatives in the output. But the result is still not good because negatives were not clamped properly before the matrix/log operation so I got some weird "posterisation" effect on negatives areas.

Conclusion

It's such frustrating to not have a simple clamp transform on OCIO v1. Right now I still don't know what to do. I know I can remove the pre-clip and only use the post-clip. With that I should got a good result on the positive range, but every negative value will be distorted. But at the same time is it really important, couldn't the user just clamp the input or should it be the config job ?

I keep investigating but for now my current solution is :

  - !<ColorSpace>
    name: AgX Log (Kraken)
    family: AgX
    equalitygroup: ""
    bitdepth: 32f
    description: AgX Log (Kraken)
    isdata: false
    allocation: uniform
    allocationvars: [ -12.47393, 4.026069 ]
    from_reference: !<GroupTransform>
      children:
        - !<MatrixTransform> { matrix: [ 0.842479062253094, 0.0784335999999992, 0.0792237451477643, 0, 0.0423282422610123, 0.878468636469772, 0.0791661274605434, 0, 0.0423756549057051, 0.0784336, 0.879142973793104, 0, 0, 0, 0, 1 ] }
        - !<AllocationTransform> { allocation: lg2, vars: [ -12.47393, 4.026069] }
        # hack to clamp to 0-1
        - !<FileTransform> { src: dummy.spi1d, interpolation: linear }
jkierbel commented 1 year ago

Most of the technical aspects of this escape me almost completely, so all I can add to the topic from here on is from the perspective of an end-user who just cares about this aestethically/artistically in a close to whatever-works kind of way, and honestly doesn't mind pixel-perfect accuracy (it's just a nice-to-know or "be adviced" that some discrepancies are there).

As I was telling another friend who's also interested in getting a functional version of AgX for software that hasn't updated to OCIO v2 yet: If this problem proves to be unsolvable (or very hard/annoying to fix) I'm personally OK with just using this config as is even prior to all the changes on this isssue, and just grading on top of the minor discrepancies (like I would grade using main-AgX anyways too so it's not really a change in my workflow, just some extra considerations for what I initially described as a "lift" on blacks). I also wonder how worth is it to struggle for a perfect match between the configs (considering how complicated it seems to be with OCIOv1's lack of clamping solutions) on the basis of getting a sort of "legacy" interchange functionality. What I mean is, Serif might add OCIO v2 support tomorrow for Affinity and make all of this a total non-issue for me, and other softwares are expected to upgrade to v2 eventually as well.

Just to test further and to see the differences on a real case scenario rather than test images of swatches and such, I grabbed an exr from an old piece of mine and pass it through Nuke + AgX main (OCIOv2) and Affinity + AgX fork (OCIOv1), and then did grading to both in their respective softwares, here are the results:

EXR loaded to Nuke with OCIO v2, main AgX base by Troy as the view-transform, no grading: testagx_03rnk

EXR loaded to Affinity Photo 2 with OCIO v1, forked AgX base by Liam as the view-transform, no grading: testagx_04raf

In this ungraded versions we can see the minor difference of the dark areas being sligtly "lifted" on the later one as we saw on the other tests.

Now l did grading to each of them:

Nuke OCIO v2 AgX main + Grading testagx_01nk

Affinity Photo 2 OCIO v1 AgX fork + Grading testagx_02af2

Here them not beign 100%/1:1 identical it's mostly due to the fact that I don't have the exact same grading tools in both sofwares, but for all intents and purposes I care about, they both work the same and that difference from the ungraded versions is gone.

And to test further:

same exr (no grading), +5 stops of exposure, with AgX main OCIOv2 as view transform, on Nuke: image

same exr (no grading), +5 stops of exposure, with AgX fork OCIOv1 as view transform, on Affinity Photo 2: image

Again showing very minor differences of the same order. At this point I'm leaning on the opinion that the difference is negligible in practical terms and if OCIO v1's tools are too limited to solve this in a reasonably simple way, leaving this config as is, labeling this as a "know bug" and moving on doesn't sound crazy to me. Just my opinion of course, I'm not the one putting the work behind this. Thanks a million for that to you @MrLixm, and @sobotka.

MrLixm commented 1 year ago

Thanks for taking further time to test, that's cool ! Even if you fall in the category of people who don't care if the color system has flaws and you just fix them in post, I can't just accept this. Color-management is already hard enough to not ignore issue even when they are "barely visible". What you found is to my eye a major issue and must be fixed.

For now, I have a solution that fix the blacks as mentioned above. But it still doesn't handle negative values properly. Negative values fall into a deep topic that I'm not enough familiar with to properly know how to handle them. I could just go, meh don't care they are not visible anyway but again I want to care for this kind of thing. So I still want to try a solution for them.

But anyway in the end I will have a fix that at least doesn't destroy the pure black values. Cheers !

sobotka commented 1 year ago

I think that the LUT was the only way to get a clip in, but I have put out a seance table with the hope of bridging to the Dark Underworld and summoning the High Priest of Colour Netherworld to confirm. His wisdom dwarfs mere mortals, so hopefully he will appear…

It is funny how almost everything comes back to invalid signals.

Short term hack would be to bookend the result with the LUT hack?

MrLixm commented 1 year ago

I hope the invocation doesn't imply too much sacrifices haha.

I don't get fully the "bookend", I can't use the LUT clip hack before because well I have to use <AllocationTransform> to preserve the dynamic-range, but those also introduced negatives ...

Right now I can use it after the log2 transform, which indeed clip negatives, but I still have artefact from the pre negatives that were not clipped ...

You can check the implementation I have right now in https://github.com/MrLixm/AgXc/pull/10/files#diff-d9156f1e95f961d581756e4f6a2850a1dd78951349122f42656fa2c74b219c9f

sobotka commented 1 year ago

The values that are outside of the chromaticity footprint of BT.709 are negative. This needs to happen before the conversion to the log encoding, if I am reading you correctly.

Only meaningful values can exist in the normalized log encoding, and with a normalized log encoding, minimum to maximum are mapped zero to one.

Using the idea of a normalized log, it would seem impossible to generate negatives.

jkierbel commented 1 year ago

Awesome, I just tried the updated config doing the same "diff pass" test as I did on the original post and now it's zeros all around on my end too, 1:1 match, looking fine on Affinity as well (I assume the lack of negative values on my test images doesn't show the remaining miss-match you mention).

Should I hit that "Close issue" button down here? Sorry I'm not very GitHub savvy.

MrLixm commented 1 year ago

@sobotka hey, Sounds like what I get yes @jkierbel thanks for confirming, no need to close it, it will be closed automatically once I merge #10 with the fix.

I consider the initial issue fixed but there is still one on negatives. So I opened a new issue https://github.com/MrLixm/AgXc/issues/11 to track it. Feel free to move on to this issue if you want to continue the conversation.

Thanks to both of you for the involvement !