Scripting with Colour Adjustment filter layer

Hi

I’m trying to use Colour Adjustment in scripting.

For all filter layer types the method I use to determinate how to set parameters is very simple.

Example with a filter layer Gaussian blur

from krita import *

doc=Krita.instance().activeDocument()
# get current active node (assume it's a filter layer)
an=doc.activeNode()
# get filter object
f=an.filter()

# get filter configuration
cfg=f.configuration()
# print configuration
print(f, f.name(), cfg.properties())

Returns:

<PyKrita.krita.Filter object at 0x7f2bf83515e0> gaussian blur {'horizRadius': '4.6', 'legacy': '', 'lockAspect': 'true', 'vertRadius': '4.6'}

So now it’s easy to change filter layers properties, as I know that we have 'horizRadius' and 'vertRadius' properties:

cfg.setProperty('horizRadius', 10)
cfg.setProperty('vertRadius', 15)
f.setConfiguration(cfg)
an.setFilter(f)
f.startFilter(an, 0, 0, doc.width(), doc.height())

But for a Colour Adjustment it doesn’t works
Returned info are:

<PyKrita.krita.Filter object at 0x7f2bf8351670> perchannel {}

So, looking how XML exported parameters for a filter are defined, I tried things like this:

c0=[(0,0), (116,55), (255,255)]
c=[(0,0), (255,255)]

filterConfig=InfoObject()
filterConfig.setProperties({
        'nTransfers': 8,
        'curve0': c0,
        'curve1': c,
        'curve2': c,
        'curve3': c,
        'curve4': c,
        'curve5': c,
        'curve6': c,
        'curve7': c
    })
c0="0,0;116,55;255,255;"
c="0,0;255,255;"

filterConfig=InfoObject()
filterConfig.setProperties({
        'nTransfers': 8,
        'curve0': c0,
        'curve1': c,
        'curve2': c,
        'curve3': c,
        'curve4': c,
        'curve5': c,
        'curve6': c,
        'curve7': c
    })

But it doesn’t work.

So, I’m wondering if this filter parameters can be set like other filters through scripting.
And if yes, how…? :slight_smile:
And if it’s not possible, is it a bug or something that hasn’t been implemented?

Note: same problem with Cross-channel filter, that also use curves…
<PyKrita.krita.Filter object at 0x7f2bf83515e0> crosschannel {}

Grum999

3 Likes

Hi @Grum999

Were you able to figure this out?
If yes, I would really really really desperately appreciate if you can spare your knowledge.

Yes, there is at least other thread talking about this. For these filters the issue is that they use 2 mechanisms to store the data. The properties one and then they use c++ objects internally also (for caching mainly iirc). Python uses the properties method, but those don’t update the objects, so that’s thr real issue. It is not a bug from c++ perspective though. But it should be changed so that both ways interoperate better.

Hi

I’ve never been able to get this working.
Some topics asked similar question, but for now there no real solution

Here a dirty workaround has been provided, but it’s dirty… :upside_down_face:

Grum999

@Grum999 see if you can test this MR: Make the per channel and cross channel filters work with python (!2134) · Merge requests · Graphics / Krita · GitLab

Hopefully it solves the issue (per channel and cross channel filters).

Edit: You have to set the “nTransfers” first in case it is 0 (may happen if the filter is created from scratch, not obtained from a filter layer) to the number of channels (8 for rgba + “all channels”, “hue”, “saturation” and “lightness”), and then set the curve parameters and the driver parameters (only in cross channel).

4 Likes

Ah thanks!
I’ll try it this week-end :slight_smile:

Grum999

Hi

Started to test, not much change

My current test (assuming the active node here is a Filter layer “per channel”)

  1. get active node
  2. get current configuration
  3. update configuration
  4. apply configuration
from krita import *

doc=Krita.instance().activeDocument()
# get current active node (assume it's a filter layer)
an=doc.activeNode()
# get filter object
f=an.filter()

# get filter configuration
cfg=f.configuration()
# print configuration
print("--1", f, f.name(), cfg.properties())

cfg.setProperty('nTransfers', 8)
cfg.setProperty('curve0', '0,0;0.1,0.1;0.9,0.9;1,1;')
cfg.setProperty('curve1', '0,0;1,1;')
cfg.setProperty('curve2', '0,0;1,1;') 
cfg.setProperty('curve3', '0,0;1,1;') 
cfg.setProperty('curve4', '0,0;1,1;') 
cfg.setProperty('curve5', '0,0;1,1;') 
cfg.setProperty('curve6', '0,0;1,1;') 
cfg.setProperty('curve7', '0,0;1,1;')

f.setConfiguration(cfg)
print("--2", f, f.name(), cfg.properties())

an.setFilter(f)

doc.refreshProjection()


f2=an.filter()
cfg2=f2.configuration()
print("--3", f2, f2.name(), cfg2.properties())

output:

--1 <PyKrita.krita.Filter object at 0x7f8ed4678940> perchannel {'curve0': '0,0;1,1;', 'curve1': '0,0;1,1;', 'curve2': '0,0;1,1;', 'curve3': '0,0;1,1;', 'curve4': '0,0;1,1;', 'curve5': '0,0;1,1;', 'curve6': '0,0;1,1;', 'curve7': '0,0;1,1;', 'nTransfers': 8}
--2 <PyKrita.krita.Filter object at 0x7f8ed4678940> perchannel {'curve0': '0,0;0.1,0.1;0.9,0.9;1,1;', 'curve1': '0,0;1,1;', 'curve2': '0,0;1,1;', 'curve3': '0,0;1,1;', 'curve4': '0,0;1,1;', 'curve5': '0,0;1,1;', 'curve6': '0,0;1,1;', 'curve7': '0,0;1,1;', 'nTransfers': 8}
--3 <PyKrita.krita.Filter object at 0x7f8ed4678a60> perchannel {'curve0': '0,0;1,1;', 'curve1': '0,0;1,1;', 'curve2': '0,0;1,1;', 'curve3': '0,0;1,1;', 'curve4': '0,0;1,1;', 'curve5': '0,0;1,1;', 'curve6': '0,0;1,1;', 'curve7': '0,0;1,1;', 'nTransfers': 8}

The initial filter settings seems unchanged.
For data input, I use the same format than for data output

If I change the initialisation of cfg with cfg = InfoObject()
Then output is:

--1 <PyKrita.krita.Filter object at 0x7f8ed4678940> perchannel {}
--2 <PyKrita.krita.Filter object at 0x7f8ed4678940> perchannel {'curve0': '0,0;0.1,0.1;0.9,0.9;1,1;', 'curve1': '0,0;1,1;', 'curve2': '0,0;1,1;', 'curve3': '0,0;1,1;', 'curve4': '0,0;1,1;', 'curve5': '0,0;1,1;', 'curve6': '0,0;1,1;', 'curve7': '0,0;1,1;', 'nTransfers': 8}
--3 <PyKrita.krita.Filter object at 0x7f8ed46789d0> perchannel {'curve0': '0,0;1,1;', 'curve1': '0,0;1,1;', 'curve2': '0,0;1,1;', 'curve3': '0,0;1,1;', 'curve4': '0,0;1,1;', 'curve5': '0,0;1,1;', 'curve6': '0,0;1,1;', 'curve7': '0,0;1,1;', 'nTransfers': 8}

There’s 2 things I think:
1) the call to updateTransfers() probably need to be made after the update property from python? (to recalculate curves values after they’ve been updated, if I understand this part)
2) I’ve put a qDebug() before line 132: it’s called when layer is initialised, it’s called if I modify curve from
Krita’s UI, but it’s never called when the values are modified from Python

→ Ok here in fact, I understand it’s to set properly data to return to Python :slight_smile:
So this part works; modifying curves in Krita’s UI and then calling filter layer setup returns curve information as expected

– I’m trying to find how the call to setCurves() can be made from Python API, but I’m currently lost in a kind of maze :sweat_smile:
I’ll continue to search this afternoon

Note: I’ve also tested to “cross channel” and it doesn’t work too

Grum999

In your example, it may be the an.setFilter(f) that is reseting something (couldn’t test yet). Try removing that line, it should update the config properly, like the config object is a reference.

Ok, I’m totally dumb… :woozy_face:

Yes it works! :partying_face:

I’m not sure how (I’m not able to understand what happen in Krita’s code here) but if setFilter() is not called, the filter layer is updated.

To be more precise, this is enough:

f=an.filter()
cfg=f.configuration()
cfg.setProperties({....})
doc.refreshProjection()

No need to set configuration to filter, neither to set filter to layer.
It’s disturbing.

Trying to create a configuration from scratch doesn’t work (with or without setFilter)

f=an.filter()
cfg=InfoObject()
cfg.setProperties({....})
f.setConfiguration(cfg)
doc.refreshProjection()

Grum999

1 Like

The info object stores a pointer to a properties object. So, even if you copy it, the properties object is shared (same pointer) between info objects, and modifying in one place affects the other, kind of reference semantics. If that’s desirable or not is another topic

The issue is that the pointer to the properties object actually points to a subclass of the base properties class. Most filters, if not all, use a subclass to store additional info.
In the case of perchannel and crosschannel filter they store additional c++ objects that are used directly instead of using the properties. That is unfortunate and may even go back to before there was python support. There may be even more filters that do that.
So you cannot just use a plain properties object at the moment. You have to obtain the filter configuration from the specific filter, which will have properties object of the correct subclass:

f = Krita.instance().filter("perchannel")
cfg = f.configuration()

Although that may crash if the filter “f” is destroyed… Couldn’t test yet.
So maybe it’s better to just get the config from the node you want to change, instead of creating a new one, unless you’re gonna apply thr filter to a paint layer or something, just in case.


The whole system is a mess, I guess because of 20 years of multiple changes. And there is no consistency between different filters with respect to how they handle the configuration.

1 Like

So this works (assuming the active node is a filter layer that has been created manually before):

from krita import *
doc=Krita.instance().activeDocument()
# get current active node (assume it's a 'perchannel' filter layer)
f=doc.activeNode().filter()
# get filter configuration
cfg=f.configuration()
cfg.setProperty('nTransfers', 8)
cfg.setProperty('curve0', '0,0;0.5,1;1,0;')
doc.refreshProjection()

And to create a new filter layer, using Krita.instance().filter() is the solution yes:

from krita import *
doc=Krita.instance().activeDocument()
f = Krita.instance().filter("perchannel")
s = Selection()
s.selectAll(doc.rootNode(), 255)
n=doc.createFilterLayer("Test", f, s)
doc.rootNode().addChildNode(n, None)
# here need to retrieve filter from layer, using the one (filter 'f' here) that has been created does nothing
f2=n.filter()
cfg = f2.configuration()
cfg.setProperty('nTransfers', 8)
cfg.setProperty('curve0', '0,0;0.5,1;1,0;')
doc.refreshProjection()

It also works for filter mask

For me even if setFilter() can’t be used, we can consider your solution works.

I’m currently trying to improve a little bit Python API, I’ll try to add some additional examples like these because it’s not totally intuitive not use the setFilter() :upside_down_face:

(I’ll add the same comment on MR)

Grum999

The problem is that info object shares the properties internally, when an info object is copied, it only copies the properties object pointer, it does not make a deep copy (clone). So modifying one info object can modify other copied info objects.

On the other hand, when the filter is passed to the create filter layer method, the properties are cloned, so modifying the created filter configuration does not modify the configuration of the filter used by the layer.

Just to clarify, the properties object maintained by the info object internally is the actual class used by krita internally, which may be a subclass of the base properties class, depending on the filter, hence it is a pointer. It is not the same as the properties returned by filter.configuration().properties(), which is just a map<string, QVariant> with the properties copied in.

@Deif_Lou
Thank you so much for your PR! It works like charm!:pray::pray::pray:

@Grum999
And thank you so much for your samples codes. They really helped me a lot!

1 Like