A Grayscale color option overhaul

Hi all,

I know I’m late to the party, but I wanted to chip in. Color is not my specialty, so if anyone knows more about it, correct away. I looked at this Wikipedia section on HSL and HSV. Given this image, the default Aseprite gray conversion is

which looks like luminance per rec. 709 with a gamma adjustment of 1.0.

Then I wrote this script as a test against the two controls (Aseprite default and Wikipedia) using this table as reference.

The example script below implements 601 and 709.

local dlg = Dialog { title = "Luminance Gray" }

dlg:combobox {
    id = "standard",
    label = "Standard:",
    option = "REC_709",
    options = { "REC_601", "REC_709" }
}

dlg:number {
    id = "gamma",
    label = "Gamma Correction:",
    text = string.format("%.1f", 1.0),
    decimals = 5
}

dlg:button {
    id = "ok",
    text = "OK",
    focus = true,
    onclick = function()
        local args = dlg.data
        if args.ok then

            -- Rec. 709
            local rcoeff = 0.2126
            local gcoeff = 0.7152
            local bcoeff = 0.0722

            if args.standard == "REC_601" then
                rcoeff = 0.2989
                gcoeff = 0.587
                bcoeff = 0.114
            end

            local gm = args.gamma

            local sprite = app.activeSprite
            if sprite then
                local srcLyr = app.activeLayer
                if srcLyr and not srcLyr.isGroup then
                    local srcCel = app.activeCel
                    if srcCel then
                        local srcImg = srcCel.image
                        local srcItr = srcImg:pixels()

                        local i = 1
                        local px = {}
                        for srcClr in srcItr do
                            local hex = srcClr()
                            local b = (hex >> 0x10 & 0xff) / 255.0
                            local g = (hex >> 0x08 & 0xff) / 255.0
                            local r = (hex >> 0x00 & 0xff) / 255.0

                            r = r ^ gm
                            g = g ^ gm
                            b = b ^ gm

                            local lum = rcoeff * r
                                      + gcoeff * g
                                      + bcoeff * b
                            local lum255 = math.tointeger(0.5 + lum * 255.0)
                            local aMask = hex & 0xff000000
                            local grayclr = aMask | lum255 << 0x10
                                                  | lum255 << 0x08
                                                  | lum255
                            px[i] = grayclr
                            i = i + 1
                        end

                        local trgLyr = sprite:newLayer()
                        trgLyr.name = args.standard
                        local trgCel = sprite:newCel(trgLyr, srcCel.frame)
                        local trgImg = trgCel.image
                        local trgItr = trgImg:pixels()
                        i = 1
                        for trgClr in trgItr do
                            trgClr(px[i])
                            i = i + 1
                        end

                        app.activeLayer = srcLyr
                        app.activeCel = srcCel
                        app.refresh()
                    else
                        app.alert("There is no active cel.")
                    end
                else
                    app.alert("The layer is a group.")
                end
            else
                app.alert("There is no open sprite.")
            end
        end
    end
}

dlg:button {
    id = "cancel",
    text = "CANCEL",
    onclick = function()
        dlg:close()
    end
}

dlg:show { wait = false }

(There’s a bug in this script when the source image has alpha. I’m just using it for testing.)

Rec. 709 with a gamma of 1.0 gave me this. That looks like the above to me.

Rec. 601 coefficients with a gamma of 1.0 gave me this

I wasn’t sure how much the gamma mattered. The most common exponents I’ve encountered are 2.2 and 2.4 and their inverses: 1.0 / 2.2 = 0.454545…, 1./0 / 2.4 = 0.46666… Below is 709 with 1.0 / 2.2:

Does the luminance have to be corrected in relation to g (i.e., raised to 1 / g) after the r, g and b are raised to g? If so, I did that part wrong in the script above.

I don’t know if the colors of a shades widget can be updated after a dialog is opened. If so something like that could be explored as a luminance preview of colors in a palette.

lumPalette

A prototype:

local function foo()
    local pal = app.activeSprite.palettes[1]
    local len = #pal
    local clrs = {}
    local gamma = 1.0
    local rcoeff = 0.2126
    local gcoeff = 0.7152
    local bcoeff = 0.0722
    for i = 1, len, 1 do
        local srcClr = pal:getColor(i - 1)

        local rf = srcClr.red / 255.0
        local gf = srcClr.green / 255.0
        local bf = srcClr.blue / 255.0

        rf = rf ^ gamma
        gf = gf ^ gamma
        bf = bf ^ gamma

        local lum = rcoeff * rf
                  + gcoeff * gf
                  + bcoeff * bf
        local lum255 = math.tointeger(0.5 + lum * 255.0)
        local a255 = srcClr.alpha
        clrs[i] = Color(lum255, lum255, lum255, a255)
    end
    return clrs
end

local function bar()
    local pal = app.activeSprite.palettes[1]
    local len = #pal
    local clrs = {}
    for i = 1, len, 1 do
        local srcClr = pal:getColor(i - 1)
        clrs[i] = srcClr
    end
    return clrs
end

local dlg = Dialog { title = "Luminance Gray" }

dlg:shades {
    id = "foo",
    mode="pick",
    colors=foo()
}

dlg:newrow()

dlg:shades {
    id = "bar",
    mode="pick",
    colors=bar()
}

dlg:button {
    id = "cancel",
    text = "CANCEL",
    onclick = function()
        dlg:close()
    end
}

dlg:show { wait = false }

Replace foo with whatever you think the appropriate metric should be.

Lastly, I have indeed heard of an artist who does initial drafts in grayscale and then either (1.) places layers with different blend modes over the grayscale layer or (2.) supplies the luminance as a factor to a color gradient evaluation function. Brandon James Greer has employed this technique (in Adobe Photoshop).

Best,
Jeremy

2 Likes