Select from Color Range - How do you make the plugin do it?

Hello there! I’ve started to make a plugin, and I have a question.

You can do a new rectangular selection with select(x, y, width, height, howMuchSelected). I want to do a selection from color range using code. I could find its ID in the Action Dictionary (Which is colorrange), but I couldn’t find anything about the parameters of it and such in the Selections API. So, how do you make a plugin do Select from Color Range? What are its parameters? Thanks.

1 Like

Hi

Action just emulate keyboard and/or menu activation for Krita’s function.
There’s no possibility to provides parameters to an action.

You probably have to implement your own Color Range selection.

It probably won’t be “easy” and pure Python implementation might be slow, but the idea is:

  • Create a selection
  • Get selection pixel data
  • From layer you want to work on, get pixel data
  • With pixel data from layer, check each pixel:
    • if match color range, update the pixel in pixel data buffer from selection
  • Once all pixels are analyzed, update the pixel data of your selection

Another solution is to create a feature request to expose Krita’s color range functionality in API…
Might be a better solution (performance, manage all document color space, and sure that you’ll have the same selection than Krita) but it could according to complexity of implementation and developer’s priorities, it can take times before API is updated

Grum999

1 Like

Thanks for the answer! Is there any way to do the implementation with the hue (HSV) values?

Here a basic working example:

from krita import *
import time
from PyQt5.Qt import *

document=Krita.instance().activeDocument()
node=document.activeNode()
w=document.width()
h=document.height()

hueRangeStart=120
hueRangeEnd=180

selection = Selection()
selection.select(0, 0, w, h, 0)
selPixelData=bytearray(selection.pixelData(0, 0, w, h))

print("Layer size: ", w, h)
pixelData=bytearray(node.pixelData(0, 0, w, h))

ts=time.time()
nbMatching=0
offset=0
color=QColor()
selOffset=0
for offset in range(0,len(pixelData),4):
    # note: pixel data are returned as BGRA order and not RGBA order
    color.setRgb(pixelData[offset+2], pixelData[offset+1], pixelData[offset], pixelData[offset+3])
    if color.hue()>=hueRangeStart and color.hue()<=hueRangeEnd:
        nbMatching+=1
        selPixelData[selOffset]=255
    selOffset+=1            
        
selection.setPixelData(QByteArray(selPixelData),0,0,w,h)

document.setSelection(selection)

print("Nb pixels analyzed", offset, "matching", nbMatching, "duration", f"{round(time.time()-ts, 4)}s")

On a 1920x1080 document size, it took approximately 2.3s to create selection

Notes:

  1. Selection is properly applied to document (you can see selection mask, and painting on document you can see that brush strokes are not painted outside selection), but for an unknown reason, selection limit is not visible :thinking:
    If someone have an idea about why applied selection is not visible :slight_smile:

  2. Example works only for RGBA/8b documents, for other (16bits, 32bits, CMYK, …) you have to adapt it

Grum999

1 Like

You will have to make the selection in HSV as you want and consider those HSV values in RGB when selecting on a RGB document so you will always need to know the colour model your at before going for a select.

Also you will have to interpolate the values in HSV for the range because if you do RGB you will be selecting another set of colours but with the same end points on your pixel data.

You can check pigmento for the formulas to convert and interpolation or you can also check the site easyrgb.com for hsv in the math section.

Also I recommend having a section of fully select and at the ends of that range have an area that goes from 100% selected to 0% selected. It would make it amazing.

1 Like

@Grum999 @EyeOdin Thanks a lot!

Also I recommend having a section of fully select and at the ends of that range have an area that goes from 100% selected to 0% selected. It would make it amazing.

What do you mean by that?

From the Libkis site:

◆ setPixelData
void Selection::setPixelData 	( QByteArray value, int x, int  y, int  w, int  h  ) slot

setPixelData writes the given bytes, of which there must be enough, into the Selection.

Parameters
    value	the byte array representing the pixels. There must be enough bytes available. Krita will take the raw pointer from the QByteArray and start reading, not stopping before (w * h) bytes are read.
    x	the x position to start writing from
    y	the y position to start writing from
    w	the width of each row
    h	the number of rows to write

Definition at line 309 of file Selection.cpp.

So the byte array when it matches the color on that pixel you say how much value(from 0 to 255, or from 0% to 100% conceptually) to the select on that pixel when creating the mask. I think that is how it works.

if you take a look at Davinci Resolve you will see this in action on the Color Panel when you do a selection by color. This eases selections so they are not harsh. Some selections need to be very precise to work like when you have hair over a background with another color and color bleeds into another making the difference very faint. Davinci Resolve is literally the best color managment tool in the market and also free so I suggest you taking a look at it.


But I do think you should leave space to upgrade your code later to add more than HSV since HSV is quite bad considering the amount of black in an image. But it will be the same as HSV but using another formula to convert the selection into the RGB to do the check. HSY/HCY and LAB would be the best methods to make selections in my mind, but I am still struggling with LAB considering what I am doing so I cant fully help you there just yet. But HSL would be super easy to add after HSV and probably give better results too.

When I check the ForeGround Color I do:

if ((self.canvas() is not None) and (self.canvas().view() is not None)):
    # Current Krita Foreground Color
    fg_color = Krita.instance().activeWindow().activeView().foregroundColor()
    fg_comp_order = fg_color.componentsOrdered()
    d_cm = fg_color.colorModel()
    d_cd = fg_color.colorDepth()
    d_cp = fg_color.colorProfile()
    if d_cm == "RGBA":
        kac1 = fg_comp_order[0] # Red
        kac2 = fg_comp_order[1] # Green
        kac3 = fg_comp_order[2] # Blue

This oddly important because if you select on a mask it will have another color model and might crash.

1 Like

Conversion Formulas:

    # HSV
    def rgb_to_hsv(self, r, g, b):
        # In case Krita is in Linear Format
        if self.d_cd != "U8":
            lsl = self.lrgb_to_srgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        # sRGB to HSX
        v_min = min( r, g, b )
        v_max = max( r, g, b )
        d_max = v_max - v_min
        v = v_max
        if d_max == 0:
            h = self.angle_live
            s = 0
        else:
            s = d_max / v_max
            d_r = ( ( ( v_max - r ) / 6 ) + ( d_max / 2 ) ) / d_max
            d_g = ( ( ( v_max - g ) / 6 ) + ( d_max / 2 ) ) / d_max
            d_b = ( ( ( v_max - b ) / 6 ) + ( d_max / 2 ) ) / d_max
            if r == v_max :
                h = d_b - d_g
            elif g == v_max :
                h = ( 1 / 3 ) + d_r - d_b
            elif b == v_max :
                h = ( 2 / 3 ) + d_g - d_r
            if h < 0 :
                h += 1
            if h > 1 :
                h -= 1
        return [h, s, v]
    def hsv_to_rgb(self, h, s, v):
        # HSX to sRGB
        if s == 0:
            r = v
            g = v
            b = v
        else:
            vh = h * 6
            if vh == 6 :
                vh = 0
            vi = int( vh )
            v1 = v * ( 1 - s )
            v2 = v * ( 1 - s * ( vh - vi ) )
            v3 = v * ( 1 - s * ( 1 - ( vh - vi ) ) )
            if vi == 0 :
                r = v
                g = v3
                b = v1
            elif vi == 1 :
                r = v2
                g = v
                b = v1
            elif vi == 2 :
                r = v1
                g = v
                b = v3
            elif vi == 3 :
                r = v1
                g = v2
                b = v
            elif vi == 4 :
                r = v3
                g = v1
                b = v
            else:
                r = v
                g = v1
                b = v2
        # In case Krita is in Linear Format
        if self.d_cd != "U8":
            lsl = self.srgb_to_lrgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        return [r, g, b]
    # HSL
    def rgb_to_hsl(self, r, g, b):
        # In case Krita is in Linear Format
        if self.d_cd != "U8":
            lsl = self.lrgb_to_srgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        # sRGB to HSX
        v_min = min( r, g, b )
        v_max = max( r, g, b )
        d_max = v_max - v_min
        l = ( v_max + v_min )/ 2
        if d_max == 0 :
            h = self.angle_live
            s = 0
        else:
            if l < 0.5 :
                s = d_max / ( v_max + v_min )
            else:
                s = d_max / ( 2 - v_max - v_min )
            d_r = ( ( ( v_max - r ) / 6 ) + ( d_max / 2 ) ) / d_max
            d_g = ( ( ( v_max - g ) / 6 ) + ( d_max / 2 ) ) / d_max
            d_b = ( ( ( v_max - b ) / 6 ) + ( d_max / 2 ) ) / d_max
            if r == v_max :
                h = d_b - d_g
            elif g == v_max:
                h = ( 1 / 3 ) + d_r - d_b
            elif b == v_max:
                h = ( 2 / 3 ) + d_g - d_r
            if h < 0:
                h += 1
            if h > 1:
                h -= 1
        return [h, s, l]
    def hsl_to_rgb(self, h, s, l):
        if s == 0 :
            r = l
            g = l
            b = l
        else:
            if l < 0.5:
                v2 = l * ( 1 + s )
            else:
                v2 = ( l + s ) - ( s * l )
            v1 = 2 * l - v2
            r = self.hsl_chan( v1, v2, h + ( 1 / 3 ) )
            g = self.hsl_chan( v1, v2, h )
            b = self.hsl_chan( v1, v2, h - ( 1 / 3 ) )
        # In case Krita is in Linear Format
        if self.d_cd != "U8":
            lsl = self.srgb_to_lrgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        return [r, g, b]
    def hsl_chan(self, v1, v2, vh):
        if vh < 0 :
            vh += 1
        if vh > 1 :
            vh -= 1
        if ( 6 * vh ) < 1 :
            return ( v1 + ( v2 - v1 ) * 6 * vh )
        if ( 2 * vh ) < 1 :
            return ( v2 )
        if ( 3 * vh ) < 2 :
            return ( v1 + ( v2 - v1 ) * ( ( 2 / 3 ) - vh ) * 6 )
        return ( v1 )
    # HSY (Krita version)
    def rgb_to_hsy(self, r, g, b):
        # In case Krita is NOT in Linear Format
        if self.d_cd == "U8":
            lsl = self.srgb_to_lrgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        # sRGB to HSX
        minval = min(r, g, b)
        maxval = max(r, g, b)
        luma= (self.luma_r*r + self.luma_g*g + self.luma_b*b)
        luma_a = luma
        chroma = maxval-minval
        max_sat = 0.5
        if chroma == 0:
            hue = self.angle_live
            sat = 0
        else:
            if maxval == r:
                if minval == b:
                    hue = (g-b)/chroma
                else:
                    hue = (g-b)/chroma + 6.0
            elif maxval == g:
                hue = (b-r)/chroma + 2.0
            elif maxval == b:
                hue = (r-g)/chroma + 4.0
            hue /=6.0
            # segment = 0.166667
            segment = 1/6
            if (hue > 1.0 or hue < 0.0):
                hue = math.fmod(hue, 1.0)
            if (hue>=0.0 and hue<segment):
                max_sat = self.luma_r + self.luma_g*(hue*6)
            elif (hue>=segment and hue<(2.0*segment)):
                max_sat = (self.luma_g+self.luma_r) - self.luma_r*((hue-segment)*6)
            elif (hue>=(2.0*segment) and hue<(3.0*segment)):
                max_sat = self.luma_g + self.luma_b*((hue-2.0*segment)*6)
            elif (hue>=(3.0*segment) and hue<(4.0*segment)):
                max_sat = (self.luma_b+self.luma_g) - self.luma_g*((hue-3.0*segment)*6)
            elif (hue>=(4.0*segment) and hue<(5.0*segment)):
                max_sat =  (self.luma_b) + self.luma_r*((hue-4.0*segment)*6)
            elif (hue>=(5.0*segment) and hue<=1.0):
                max_sat = (self.luma_r+self.luma_b) - self.luma_b*((hue-5.0*segment)*6)
            else:
                max_sat=0.5

            if(max_sat>1.0 or max_sat<0.0):
                max_sat = math.fmod(max_sat, 1.0)
            if luma <= max_sat:
                luma_a = (luma/max_sat)*0.5
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5

            if (chroma > 0.0):
                sat = ((chroma/(2*luma_a)) if (luma <= max_sat) else (chroma/(2.0-(2*luma_a))))
        if sat<=0.0:
            sat=0.0
        if luma<=0.0:
            luma=0.0
        h=hue
        s=sat
        y=luma**(1/self.gamma_y)
        return [h, s, y]
    def hsy_to_rgb(self, h, s, y):
        hue = 0.0
        sat = 0.0
        luma = 0.0
        if ( h > 1.0 or h < 0.0):
            hue = math.fmod(h, 1.0)
        else:
            hue = h
        if s < 0.0:
            sat = 0.0
        else:
            sat = s
        if y < 0.0:
            luma = 0.0
        else:
            luma = y**(self.gamma_y)
        # segment = 0.166667
        segment = 1/6
        r=0.0
        g=0.0
        b=0.0
        if (hue >= 0.0 and hue < segment):
            max_sat = self.luma_r + ( self.luma_g*(hue*6) )
            if luma <= max_sat:
                luma_a = (luma/max_sat)*0.5
                chroma=sat*2*luma_a
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)

            fract = hue*6.0
            x = (1-abs(math.fmod(fract, 2)-1))*chroma
            r = chroma
            g=x
            b=0
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        elif (hue >= (segment) and hue < (2.0*segment)):
            max_sat = (self.luma_g+self.luma_r) - (self.luma_r*(hue-segment)*6)

            if luma<max_sat:
                luma_a = (luma/max_sat)*0.5
                chroma = sat*(2*luma_a)
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)

            fract = hue*6.0
            x = (1-abs(math.fmod(fract, 2)-1) )*chroma
            r = x
            g=chroma
            b=0
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        elif (hue >= (2.0*segment) and hue < (3.0*segment)):
            max_sat = self.luma_g + (self.luma_b*(hue-2.0*segment)*6)
            if luma<max_sat:
                luma_a = (luma/max_sat)*0.5
                chroma=sat*(2*luma_a)
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)
            fract = hue*6.0
            x = (1-abs(math.fmod(fract,2)-1) )*chroma
            r = 0
            g=chroma
            b=x
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        elif (hue >= (3.0*segment) and hue < (4.0*segment)):
            max_sat = (self.luma_g+self.luma_b) - (self.luma_g*(hue-3.0*segment)*6)
            if luma<max_sat:
                luma_a = (luma/max_sat)*0.5
                chroma=sat*(2*luma_a)
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)

            fract = hue*6.0
            x = (1-abs(math.fmod(fract,2)-1) )*chroma
            r = 0
            g=x
            b=chroma
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        elif (hue >= (4.0*segment) and hue < (5*segment)):
            max_sat = self.luma_b + (self.luma_r*((hue-4.0*segment)*6))
            if luma<max_sat:
                luma_a = (luma/max_sat)*0.5
                chroma=sat*(2*luma_a)
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)

            fract = hue*6.0
            x = (1-abs(math.fmod(fract,2)-1) )*chroma
            r = x
            g=0
            b=chroma
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        elif (hue >= (5.0*segment) and hue <= 1.0):
            max_sat = (self.luma_b+self.luma_r) - (self.luma_b*(hue-5.0*segment)*6)
            if (luma<max_sat):
                luma_a = (luma/max_sat)*0.5
                chroma=sat*(2*luma_a)
            else:
                luma_a = ((luma-max_sat)/(1-max_sat)*0.5)+0.5
                chroma=sat*(2-2*luma_a)

            fract = hue*6.0
            x = (1-abs(math.fmod(fract,2)-1) )*chroma
            r = chroma
            g=0
            b=x
            m = luma-( (self.luma_r*r)+(self.luma_b*b)+(self.luma_g*g) )
            r += m
            g += m
            b += m
        else:
            r=0.0
            g=0.0
            b=0.0
        if r<0.0:
            r=0.0
        if g<0.0:
            g=0.0
        if b<0.0:
            b=0.0
        # In case Krita is NOT in Linear Format
        if self.d_cd == "U8":
            lsl = self.lrgb_to_srgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        return [r, g, b]
    # HCY (My Paint Version)
    def rgb_to_hcy(self, r, g, b):
        # In case Krita is NOT in Linear Format
        if self.d_cd != "U8": # == vs !=
            lsl = self.srgb_to_lrgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        # sRGB to HSX
        y = self.luma_r*r + self.luma_g*g + self.luma_b*b
        p = max(r, g, b)
        n = min(r, g, b)
        d = p - n
        if n == p:
            h = self.angle_live
        elif p == r:
            h = (g - b)/d
            if h < 0:
                h += 6.0
        elif p == g:
            h = ((b - r)/d) + 2.0
        else:  # p==b
            h = ((r - g)/d) + 4.0
        h /= 6.0
        if (r == g == b or y == 0 or y == 1):
            h = self.angle_live
            c = 0.0
        else:
            c = max((y-n)/y, (p-y)/(1-y))
        # y = y**(1/self.gamma_y) # Gama compression of the luma value
        return [h, c, y]
    def hcy_to_rgb(self, h, c, y):
        # y = y**(self.gamma_y) # Gama compression of the luma value
        if c == 0:
            r = y
            g = y
            b = y
        h %= 1.0
        h *= 6.0
        if h < 1:
            th = h
            tm = self.luma_r + self.luma_g * th
        elif h < 2:
            th = 2.0 - h
            tm = self.luma_g + self.luma_r * th
        elif h < 3:
            th = h - 2.0
            tm = self.luma_g + self.luma_b * th
        elif h < 4:
            th = 4.0 - h
            tm = self.luma_b + self.luma_g * th
        elif h < 5:
            th = h - 4.0
            tm = self.luma_b + self.luma_r * th
        else:
            th = 6.0 - h
            tm = self.luma_r + self.luma_b * th
        # Calculate the RGB components in sorted order
        if tm >= y:
            p = y + y*c*(1-tm)/tm
            o = y + y*c*(th-tm)/tm
            n = y - (y*c)
        else:
            p = y + (1-y)*c
            o = y + (1-y)*c*(th-tm)/(1-tm)
            n = y - (1-y)*c*tm/(1-tm)
        # Back to RGB order
        if h < 1:
            r = p
            g = o
            b = n
        elif h < 2:
            r = o
            g = p
            b = n
        elif h < 3:
            r = n
            g = p
            b = o
        elif h < 4:
            r = n
            g = o
            b = p
        elif h < 5:
            r = o
            g = n
            b = p
        else:
            r = p
            g = n
            b = o
        # In case Krita is NOT in Linear Format
        if self.d_cd != "U8": # == vs !=
            lsl = self.lrgb_to_srgb(r, g, b)
            r = lsl[0]
            g = lsl[1]
            b = lsl[2]
        return [r, g, b]

The Luma Values that is missing and are variable:

        if luminosity == "ITU-R BT.601":
            # Luma Coefficients
            self.luma_r = 0.299
            self.luma_b = 0.114
            self.luma_g = 1 - self.luma_r - self.luma_b # 0.587
            self.luma_pr = 1.402
            self.luma_pb = 1.772
        if luminosity == "ITU-R BT.709":
            # Luma Coefficients
            self.luma_r = 0.2126
            self.luma_b = 0.0722
            self.luma_g = 1 - self.luma_r - self.luma_b # 0.7152
            self.luma_pr = 1.5748
            self.luma_pb = 1.8556
        if luminosity == "ITU-R BT.2020":
            # Luma Coefficients
            self.luma_r = 0.2627
            self.luma_b = 0.0593
            self.luma_g = 1 - self.luma_r - self.luma_b # 0.678
            self.luma_pr = 0.4969
            self.luma_pb = 0.7910

If you see a self.angle_live floating around it is just probably zero for your case.

1 Like

Yoooooooo that’s a lot of stuff! Thanks a lot!

Hey, um, can I use the code you wrote with credit for the plugin?

You can use it of course, otherwise I didn’t gave you a solution :slight_smile:
You don’t need to provide credit for this, I just help a few about how to use Krita’s API :wink:

Grum999

3 Likes