Understanding Colour Value with HSL/HSV

Hi, I wonder if someone can please help, this is not essential but I’m still curious. I’m trying to understand colour ‘value’ and do have been analyzing the amazing work from Metal Slug to see how they use value to separate foreground and background objects. Here’s the pic:
norm_01

When I change the ‘sprite colour mode’ to grayscale to more easily see the values of the colours, I get this:
grayscale_01

But when I go to ‘Adjustments’ and decrease the saturation (I think with HSV) I get this - quite a different result:
fx_hsv_01

I did some more experimenting and when decreasing saturation in HSL colour mode, I got the same result as changing the colour mode to grayscale.

I’m admittedly a noob and know very little about what HSL and HSV modes are, and am also partially colourblind (lol). I wonder if anyone might be able to please shed some light on whether HSL or HSV is the more accurate representation of colour ‘value’, and I’m not even sure I’m asking the right question here.

1 Like

you see hue saturation and value arent completely separete, they all cascate down changing the final value.

1 Like

HSL and HSV have different approach to ‘value’ as artists understand it (and people see it). if we have for example pure red, it’s defined as 0,100,100 in hsv but 0, 100,50 in hsl. anything with l=100 in hsl model is white.
so, when you remove saturation, each of them will get you different shade of gray (you can try it in colour mixing bar).

now, the answer to your question which one of those two examples is more accurate representation of value is: the second one, but actually neither of them. as ethan suggested human perception of value is nothing like idealized computer model.
we’ve had fun conversation about grayscale conversion some time ago here, so check it if you wish to descent into madness.

i can recommend modified version of behreandtjeremy’s script i’m posting below. it isn’t perfect (for example the orange still feels a bit too dark), but so far it’s the best option we have:
_temp

PS.
also note that value is not the only way how to separate foreground from background. aerial perspective and both hue and saturation contrasts in general work just as well. see the background on the very left edge.

app.transaction( function() 
			
            -- Rec. 709
            local rcoeff = 0.2126
            local gcoeff = 0.7152
            local bcoeff = 0.0722

            local gm = 1
            local standard = "grayscale"

            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 position = srcCel.position 

                        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 = math.sqrt(rcoeff * r^2
                                      + gcoeff * g^2
                                      + bcoeff * b^2)
                            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 = standard
                        local trgCel = sprite:newCel(trgLyr, srcCel.frame,srcImg, position )
                        
                        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)
2 Likes

Thank you so much for this thorough reply!

1 Like