Feedback / Development: Filmic, Baby Step to a V2?

Ok so I need to assume the input is logarithmic with base 2, right? Meaning I need to first, before anything else, take the XYZ grid to the power of 2?
And I take it the specific range I’m aiming for is the thing in the vars?
Let’s see what happens if I do that…

How is this?

Saturation_Compression.cube (1.1 MB)

Hmm

Can you post the updated Python? I want to see what’s going on as well.

The relevant change is just in the loop over the entire LUT.table, and in the LUT.domain. All else is the same.

import colour
import numpy as np


def subtract_mean(col):
    mean = np.mean(col)
    return col - mean, mean


def add_mean(col, mean):
    return col + mean


def cart_to_sph(col):
    r = np.linalg.norm(col)
    phi = np.arctan2(col[1], col[0])
    rho = np.hypot(col[0], col[1])
    theta = np.arctan2(rho, col[2])
    return np.array([r, phi, theta])


def sph_to_cart(col):
    r = col[0]
    phic = np.cos(col[1])
    phis = np.sin(col[1])
    thetac = np.cos(col[2])
    thetas = np.sin(col[2])
    x = r * phic * thetas
    y = r * phis * thetas
    z = r * thetac
    return np.array([x, y, z])


def compress(val, f1, fi):
    fiinv = 1 - fi
    return val * fi/(1 - fiinv * np.power(((f1*fiinv)/(f1-fi)), -val))


def transform(col, f1, fi):
    col, mean = subtract_mean(col)
    col = cart_to_sph(col)
    col[0] = compress(col[0], f1=f1, fi=fi)
    col = sph_to_cart(col)
    return add_mean(col, mean)


def main():

    f1 = 0.9
    fi = 0.8

    LUT = colour.LUT3D(name='Spherical Saturation Compression')
    LUT.domain = ([[-12.473931188, -12.473931188, -12.473931188], [ 12.473931188, 12.473931188, 12.473931188]])
    LUT.comments = [f'Spherically compress saturation by a gentle curve such that very high saturation values are reduced by {((1-fi)*100):.1f}%',
                    f'At a spherical saturation of 1.0, the compression is {((1-f1)*100):.1f}%']

    x, y, z, _ = LUT.table.shape
    for i in range(x):
        for j in range(y):
            for k in range(z):
                col = np.array(LUT.table[i][j][k], dtype=np.longdouble)
                col = np.power(2, col)
                col = transform(col, f1=f1, fi=fi)
                col = np.log2(col)
                LUT.table[i][j][k] = np.array(col, dtype=LUT.table.dtype)

    colour.write_LUT(LUT, "Saturation_Compression.cube")
    print(LUT.table)
    print(LUT)


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass

I think I see the issue. Hold on. The domain variables didn’t actually change the internals of the LUT, so I’m going for a completely wrong range.

How about this?

import colour
import numpy as np


def subtract_mean(col):
    mean = np.mean(col)
    return col - mean, mean


def add_mean(col, mean):
    return col + mean


def cart_to_sph(col):
    r = np.linalg.norm(col)
    phi = np.arctan2(col[1], col[0])
    rho = np.hypot(col[0], col[1])
    theta = np.arctan2(rho, col[2])
    return np.array([r, phi, theta])


def sph_to_cart(col):
    r = col[0]
    phic = np.cos(col[1])
    phis = np.sin(col[1])
    thetac = np.cos(col[2])
    thetas = np.sin(col[2])
    x = r * phic * thetas
    y = r * phis * thetas
    z = r * thetac
    return np.array([x, y, z])


def compress(val, f1, fi):
    fiinv = 1 - fi
    return val * fi/(1 - fiinv * np.power(((f1*fiinv)/(f1-fi)), -val))


def transform(col, f1, fi):
    col, mean = subtract_mean(col)
    col = cart_to_sph(col)
    col[0] = compress(col[0], f1=f1, fi=fi)
    col = sph_to_cart(col)
    return add_mean(col, mean)


def main():

    f1 = 0.9
    fi = 0.8

    LUT = colour.LUT3D(name='Spherical Saturation Compression')
    LUT.domain = ([[-12.473931188, -12.473931188, -12.473931188], [ 12.473931188, 12.473931188, 12.473931188]])
    LUT.comments = [f'Spherically compress saturation by a gentle curve such that very high saturation values are reduced by {((1-fi)*100):.1f}%',
                    f'At a spherical saturation of 1.0, the compression is {((1-f1)*100):.1f}%']

    x, y, z, _ = LUT.table.shape
    for i in range(x):
        for j in range(y):
            for k in range(z):
                col = np.array(LUT.table[i][j][k], dtype=np.longdouble)
                col = 2 * 12.473931188 * col - 12.473931188
                col = np.power(2, col)
                col = transform(col, f1=f1, fi=fi)
                col = np.log2(col)
                LUT.table[i][j][k] = np.array(col, dtype=LUT.table.dtype)

    colour.write_LUT(LUT, "Saturation_Compression.cube")
    print(LUT.table)
    print(LUT)


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass

Saturation_Compression.cube (1.1 MB)

If this works, I think we need to discuss the relevant resolution. Right now this is a resolution of 33×33×33, which may be excessive or it may be too low. It should be easy to change but I’m not sure what an appropriate scale might be. It’s like 1.1MB as is, and it quickly grows as the size grows. I’m not sure what an appropriate LUT size budget might be.

Seems to work but with some posterization

1 Like

That’s gonna come down to two things:

  • the resolution of the 3DLUT
  • the interpolation method

Good to see that the concept works at least. Now for the finetuning…
Any indication of what sort of resolution AgX works with?

Not sure, maybe try 64 or 128 or something?

oh wait, is AgX just a 1D LUT? It apparently uses 4096 steps, which is easy to do with a 1D LUT, but with a 3D LUT that sort of resolution would be insane…

Here is a resolution of 64.
Saturation_Compression.7z (1.3 MB)

Hmm 64 has significantly less posterization, but I observed a strange thing, I am not sure why.

Two scenarios:
1:

        - !<AllocationTransform> {allocation: lg2, vars: [-12.473931188, 12.473931188]}
        - !<FileTransform> {src: Spherical_Compression.cube, interpolation: linear}
        - !<AllocationTransform> {allocation: lg2, vars: [-12.473931188, 12.473931188], direction: inverse}

What I have been doing, apply log before and cancel after.
Result:

Seems the compression is not working?

Scenario 2:

        - !<FileTransform> {src: Spherical_Compression.cube, interpolation: linear}

Not applying any log in the config.

Result:

The compression seems to work, but it doesn’t match what I saw in the node group, it seems significantly more aggressive.

The resolution 128 version is 511MB which seems quite excessive…
I really hope we can get it to work reasonably with less than that.

I don’t see why it would possibly be more aggressive, but at least the result looks kinda reasonable? (On Matas - the dark side of the sweep is a complete mess)
Really strange…

Here is a version with resolution 91
Saturation_Compression_91.7z (4.0 MB)

wait, I got it working, so it turned out there is no need to to the power and log thing, all that needs to do is:

and in the config:

        - !<FileTransform> {src: Spherical_Compression.cube, interpolation: best}

Here you go, working perfectly:

Resolution 33 turned out to be fine, just use best as interpolation method and all good

1 Like

Strange but alright… That’s not exactly covering the range in question though, assuming the 12.something business is supposed to be the stops. You’d want 5688.8888875781081812757895086344 instead of 128, which is a much much broader range…

Ok I can change that. But doesn’t seem to matter, result looks the same

1 Like

Looking forward to hearing @troy_s’s take on these developments. I think going higher than 33 is plausible if needed, though perhaps not much higher. If it already looks good at that resolution, all the better though

I’m also not sure whether negative values even do anything in this case, since this is still linear rather than exponential. Might suffice to only map out positive values, or only go slightly negative, mostly sticking to positive ones…

Folks should find that the log shaper is required. It will cause posterization artifacts otherwise.

That is, think of the 3D LUT as a closed domain. The values should be distributed maximally, for example, so that the middle grey is more than a mere 20% of the LUT range.

The issue you had is flipping back after the 3D LUT. There’s no need. The order should be:

  • Shaper
  • 3D LUT that ingests shaped values, and spits out the proper final output.

I haven’t looked closely, but a shaper will be required to prevent odd allocations. It’s similar to the old linear values quantized to lower bit depth issues.

Given that the vast majority of the LUT should amount to a close no operation, it might make sense to use the smaller 33 cube size. I’d be cautious and test for the values that will be potentially causing problems.

It’s not EV relative to the working space. It’s log2 values, so any multiplication needs to be accounted for. There’s a post here on the forums where I walk over how the allocation values are derived.

Sorry that I can’t look more closely for now.

1 Like

Ok so then the first version up here

ought to have been the right version, and all that’s necessary is not to undo the lg2? (and use best interpolation)

I thought EV also are logarithmic? Isn’t EV+1 doubling the brightness of the image?
Either way, I meant log2 values anyway

Hmm but it is currently working perfectly, not sure why, but it is working without any artifact. Cannot get it to work otherwise.

so just leaving out the inverse transform after doesn’t change anything?

No it has some weird problems.

Also I discovered the current implementation causes problems for the contrast looks. I would need to investigate more…

This is what happens if I just do that:

Weird hue shift + looking like it’s not properly de-logged