Hue(), Saturation() and Luminosity() [full code]

Just starting out? Need help? Post your questions and find answers here.
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Hue(), Saturation() and Luminosity() [full code]

Post by Seymour Clufley »

For some time now I've been desirous of three generic procedures to complement Red, Green, Blue (and now Alpha, of course).

Based on code from Froggerpogger, I've written these procedures. Each of them takes in an RGB colour, translates it to HSL format then returns the H,S or L value. So, Luminosity() is as simple to use as Green().

To try and speed up the procedures, I've implemented a "memory bank". When an RGB is translated to HSL, it is added to an array structure so that it doesn't need to be translated again. This is sensible because you may call Saturation() and Hue() in succession for the same colour, so translating it twice would be ridiculous. A maximum of 100 colours can be stored in this array - then it begins re-translating and re-using slots. There may be a better way to do this, but I've never used Lists or Maps so can't tell.

So, ignoring the structures for now, the system works like this:

Code: Select all

Procedure.i TranslateRGBtoHSL(rgb.i)
  
  // translation code
  // ...
  // ...
  
  // save into memory bank code
  // ...
  // ...
  
  ; return bank slot number
  ProcedureReturn slot
  
EndProcedure


Macro CheckTranslateRGBtoHSL(c)
  slot = 0
  
  // check for colour in the memory bank
  //...
  
  If Not slot ; colour isn't in the memory bank, so needs translating
      slot = TranslateRGBtoHSL(c)
  EndIf
EndMacro


Procedure.d Hue(c.i) ; saturation and luminosity work exactly the same
  CheckTranslateRGBtoHSL(c)
  ProcedureReturn hm\slot[slot]\hue
EndProcedure
The full code is at the bottom of this post.

Additionally, these functions are pretty useless without a way to convert back to RGB. (Then we can play in the HSL colourspace and still use PB's drawing functions.)

So using code by Froggerpogger I've made the procedure HSL(hue.i,sat.i,lum.i) which returns an RGB value. Again, combining H,S,L values into a usable RGB integer is as simple as combining R,G,B values with the RGB() function.

Now here's the complete code, including a demo program wherein 4 squares are drawn. Blue, yellow, green and red. A custom filter callback is used whereby yellow pixels are desaturated 75%.

Incidentally, the first 3 procedures in the code are standard proc's that I use in almost every program. RI() is especially useful for seeing how an image gets processed by the code you're working on - press Escape, Space or Return to close the image window once it appears.

Code: Select all

Procedure.d Beat(a.d,b.d)
  If a>b
      ProcedureReturn a
  Else
      ProcedureReturn b
  EndIf
EndProcedure

Procedure.d Defeat(a.d,b.d)
  If a<b
      ProcedureReturn a
  Else
      ProcedureReturn b
  EndIf
EndProcedure


Procedure RI(img.i,title.s)
  
  If Not IsImage(img)
      MessageRequester("RI error","img is not an image!",0)
      ProcedureReturn
  EndIf
  
  iw = ImageWidth(img)
  ih = ImageHeight(img)
  
  If Not title
      title = "Report Image"
  EndIf
  
  win = OpenWindow(#PB_Any,0,0,iw,ih,title,#PB_Window_BorderLess|#PB_Window_ScreenCentered)
  
  imgad = ImageGadget(#PB_Any,0,0,iw,ih,ImageID(img))
  
  escapekey = 1
  spacekey = 2
  returnkey = 3
  AddKeyboardShortcut(win,#PB_Shortcut_Escape,escapekey)
  AddKeyboardShortcut(win,#PB_Shortcut_Space,spacekey)
  AddKeyboardShortcut(win,#PB_Shortcut_Return,returnkey)
  
  Repeat
      we = WindowEvent()
      If we
          If we=#PB_Event_Menu
              Break
          EndIf
      Else
          Delay(10)
      EndIf
  ForEver
  
  CloseWindow(win)
  
  If free
      FreeImage(img)
  EndIf
  
EndProcedure





;-
;- INTO HSL STUFF

Structure hsl_slot
  rgb.i
  
  hue.d
  sat.d
  lum.d
EndStructure
#MaxHSLRems = 100
Structure HSL_MemBank
  slot.hsl_slot[#MaxHSLRems+1]
  useslot.i
  highestused.i
EndStructure
Global hm.HSL_MemBank

Procedure.d Max3D(d1.d,d2.d,d3.d)
  
  bd.d = d1
  
  bd = Beat(bd,d2)
  
  bd = Beat(bd,d3)
  
  ProcedureReturn bd
  
EndProcedure

Procedure.d Min3D(d1.d,d2.d,d3.d)
  
  bd.d = d1
  
  bd = Defeat(bd,d2)
  
  bd = Defeat(bd,d3)
  
  ProcedureReturn bd
  
EndProcedure

Procedure.i TranslateRGBtoHSL(c.i)
  
  PrintN("Translate begin @ "+Str(ElapsedMilliseconds()))
  
  hm\useslot+1
  If hm\useslot>#MaxHSLRems
      hm\useslot=1
  EndIf
  hm\highestused = Beat(hm\useslot,hm\highestused)
  
  m = hm\useslot
  
  hm\slot[m]\rgb = c
  hm\slot[m]\hue = 0
  hm\slot[m]\sat = 0
  hm\slot[m]\lum = 0
  
  
  Select c
      Case #Black
          ProcedureReturn m
      Case #Gray
          hm\slot[m]\lum = 128
          ProcedureReturn m
      Case #White
          hm\slot[m]\lum = 255
          ProcedureReturn m
  EndSelect
  
  r.d = Red(c)
  g.d = Green(c)
  b.d = Blue(c)
  
  max.d = Max3D(r,g,b)
  min.d = Min3D(r,g,b)
  delta.d = max - min
  
  
  hue.d
  sat.d
  lum.d
  
  ; get lightness
  lum = (max + min) / 2.0
  
  ; get saturation
  If lum <= 0.5
      sat = delta/(max+min)
  Else
      sat = delta/(2-max-min)
  EndIf
  
  ; get hue
  If r = max
      hue = (g-b)/delta
  ElseIf g = max
      hue = 2.0 + (b-r)/delta
  ElseIf b = max
      hue = 4.0 + (r-g)/delta
  EndIf
  
  hue * 60.0
  If hue<0.0
      hue + 360.0
  EndIf
  
  hm\slot[m]\hue = hue
  hm\slot[m]\sat = sat
  hm\slot[m]\lum = lum
  
  PrintN("Translate finished @ "+Str(ElapsedMilliseconds()))
  ProcedureReturn m
  
EndProcedure

Macro CheckTranslateRGBtoHSL(c)
  slot = 0
  If hm\highestused>0 ; no point doing this search if no slots have been filled yet (no colours translated)
      For a = 1 To hm\highestused
          If hm\slot[a]\rgb = c
              ;printn "using old"
              slot = a
              Break
          EndIf
      Next a
  EndIf
  If Not slot
      slot = TranslateRGBtoHSL(c)
      PrintN("Stored in slot #"+Str(slot))
  EndIf
EndMacro

Procedure.d Hue(c.i)
  
  CheckTranslateRGBtoHSL(c)
  ProcedureReturn hm\slot[slot]\hue
  
EndProcedure

Procedure.d Saturation(c.i)
  
  CheckTranslateRGBtoHSL(c)
  ProcedureReturn hm\slot[slot]\sat
  
EndProcedure

Procedure.d Luminosity(c.i)
  
  CheckTranslateRGBtoHSL(c)
  ProcedureReturn hm\slot[slot]\lum
  
EndProcedure


;-
;- INTO RGB STUFF

Procedure.f HSL2RGBHelper(q1.f, q2.f, h.f)
  If h >= 360.0
    h - 360.0
  ElseIf h < 0.0
    h + 360.0
  EndIf
  
  If h < 60.0
    ProcedureReturn q1+(q2-q1)*h/60.0
  ElseIf h < 180.0
    ProcedureReturn q2
  ElseIf h < 240.0
    ProcedureReturn q1+(q2-q1)*(240.0-h)/60.0
  Else
    ProcedureReturn q1
  EndIf
EndProcedure

Procedure.i HSL(hue.d,sat.d,lum.d) ; returns an RGB value usable in PB drawing operations
  Protected f.f, p1.d, p2.d, t.f 
  Protected i.l 
  
  If lum<=0.5
    p2 = lum*(1.0+sat)
  Else
    p2 = lum+sat-lum*sat
  EndIf
  
  p1 = 2.0*lum-p2
  
  If sat=0.0
    ; it's a gray-tone
    r = lum
    g = lum
    b = lum
  Else
    r = HSL2RGBHelper(p1, p2, hue+120.0)
    g = HSL2RGBHelper(p1, p2, hue)
    b = HSL2RGBHelper(p1, p2, hue-120.0)
  EndIf
  
  ProcedureReturn RGB(r,g,b)
  
EndProcedure









;-
;- THE CALLBACK

Procedure.i QuarterYellowSaturation(x.i,y.i,tc.i,bc.i)
  
  bh.d = Hue(bc)
  
  
  nrgb = bc
  If bh=60 ; hue value for yellow is 60
      bs.d = Saturation(bc)
      bl.d = Luminosity(bc)
      abs.d = bs/4
      nrgb = HSL(bh,abs,bl)
  EndIf
  
  ProcedureReturn nrgb
  
EndProcedure






;-
;- PROGRAM START

OpenConsole()

iw.d = 900
ih.d = 900
h.d = iw/2

img = CreateImage(#PB_Any,iw,ih)

StartDrawing(ImageOutput(img))
    Box(0,0,h,h,#Green)
    Box(0,h,h,h,#Blue)
    Box(h,0,h,h,#Yellow) ; this will be augmented by the custom filter
    Box(h,h,h,h,#Red)
    
    DrawingMode(#PB_2DDrawing_CustomFilter)
    CustomFilterCallback(@QuarterYellowSaturation())
    start = ElapsedMilliseconds()
    Box(0,0,iw,ih)
    stop = ElapsedMilliseconds()
StopDrawing()

RI(img,Str(stop-start)+" ms")

Only bummer is that if the debugger is on, runtime takes a pounding from 79ms to 1953ms. So I've changed Debug calls to PrintN calls and done without the debugger.

Now, I'm personally quite happy with this code, but can anyone think of ways to improve it or speed it up? 79ms does seem lengthy for a very simple operation.

Otherwise, just enjoy the Hue(), Saturation(), Luminosity() and HSL() procedures!

All the best,
Seymour.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
citystate
Enthusiast
Enthusiast
Posts: 638
Joined: Sun Feb 12, 2006 10:06 pm

Post by citystate »

dunno about speeding it up, but have you considered changing the slots to a Hashmap?
there is no sig, only zuul (and the following disclaimer)

WARNING: may be talking out of his hat
eesau
Enthusiast
Enthusiast
Posts: 589
Joined: Fri Apr 27, 2007 12:38 pm
Location: Finland

Post by eesau »

Have you tried using floats instead of doubles? I'm not sure but I think double-accuracy is not really needed when doing HSL, and using floats should speed it up a bit.

Edit:
79ms does seem lengthy for a very simple operation.
It does, but that's how CustomFilterCallback works. It calls the procedure for each pixel, ie. for a 900x900 image it makes 810 000 calls, which makes it slow.
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Post by Seymour Clufley »

Code: Select all

It calls the procedure for each pixel
Yes, this is unfortunate, because it really piles on the runtime.

EDIT: Feature Request posted.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Post by Seymour Clufley »

eesau wrote:Have you tried using floats instead of doubles? I'm not sure but I think double-accuracy is not really needed when doing HSL, and using floats should speed it up a bit.
Thanks for the suggestion. I tried it but it only reduced the time to 63ms!
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Hue(), Saturation() and Luminosity() [full code]

Post by Seymour Clufley »

13 years later, a much improved version.

Note that the saturation and luminosity values are not 0->100 but 0->255. The hue value is 0->360.

Conversion to HSL and back again seems to be exact. I have included some code to demonstrate this.

HSLA is also supported. If you include an alpha parameter with HSL(), it returns a 32-bit RGBA integer. Otherwise, it returns a 24-bit RGB integer with no alpha channel.

Code: Select all

Structure HSLA_Structure
  hue.d
  sat.d
  lum.d
  alpha.d
EndStructure
Global NewMap hsla_mem.HSLA_Structure()

Macro CheckHSLAMem(prgb2hslclrt)
  If Not FindMapElement(hsla_mem(),prgb2hslclrt)
    prgb2hslclr = Val(prgb2hslclrt)
    
    r.d = Red(prgb2hslclr) / 255
    g.d = Green(prgb2hslclr) / 255
    b.d = Blue(prgb2hslclr) / 255
    Dim a.d(2)
    a(0) = r : a(1) = g : a(2) = b
    SortArray(a(),#PB_Sort_Ascending )
    cmax.d = a(2)
    cmin.d = a(0)
    lum.d = (cmax + cmin) / 2
    If cmax = cmin
      hue.d = 0
      sat.d = 0
    Else
      c.d = cmax - cmin
      sat = c / (1 - Abs(2 * lum - 1))
      If r = cmax
        hue = (g - b) / c
      ElseIf g = cmax
        hue = (b - r) / c + 2
      ElseIf b = cmax
        hue = (r - g) / c + 4
      EndIf
    EndIf
    
    hsla_mem(prgb2hslclrt)\hue = hue*60
    While hsla_mem(prgb2hslclrt)\hue>359
      hsla_mem(prgb2hslclrt)\hue-360
    Wend
    While hsla_mem(prgb2hslclrt)\hue<0
      hsla_mem(prgb2hslclrt)\hue+360
    Wend
    
    
    hsla_mem(prgb2hslclrt)\sat = sat*255
    hsla_mem(prgb2hslclrt)\lum = lum*255
    hsla_mem(prgb2hslclrt)\alpha = Alpha(prgb2hslclr)
  EndIf
EndMacro


Procedure.d Hue(clr.i)
  clrt.s = Str(clr)
  CheckHSLAMem(clrt)
  ProcedureReturn hsla_mem(clrt)\hue
EndProcedure

Procedure.d Saturation(clr.i)
  clrt.s = Str(clr)
  CheckHSLAMem(clrt)
  ProcedureReturn hsla_mem(clrt)\sat
EndProcedure

Procedure.d Luminosity(clr.i)
  clrt.s = Str(clr)
  CheckHSLAMem(clrt)
  ProcedureReturn hsla_mem(clrt)\lum
EndProcedure


Procedure.d Hue_2_RGB(v1.d, v2.d, vH.d)
   If vH < 0 : vH + 1 : EndIf
   If vH > 1 : vH - 1 : EndIf
   If (6 * vH) < 1 : ProcedureReturn (v1 + (v2 - v1) * 6 * vH) : EndIf
   If (2 * vH) < 1 : ProcedureReturn v2 : EndIf
   If (3 * vh) < 2 : ProcedureReturn (v1 + (v2 - v1) * ((2.0 / 3.0) - vH) * 6) : EndIf
   
   ProcedureReturn v1
EndProcedure

Macro ContainThis(num,min,max)
  If num<min
      num=min
  EndIf
  If num>max
      num=max
  EndIf
EndMacro

Procedure.i HSL(h.d,s.d,l.d,alpha.d=#PB_Ignore)
  ; input variables
  ; (H)ue        0 --> 360.0
  ; (S)aturation 0 --> 255.0
  ; (L)ightness  0 --> 255.0
  
  While h>359
    h-360
  Wend
  While h<0
    h+360
  Wend
  ContainThis(s,0,255)
  ContainThis(l,0,255)
  
  h/360
  s/255
  l/255
  
  If s = 0
    R.i = l * 255
    G.i = l * 255
    B.i = l * 255
  Else
    If l < 0.5
      var_2.d = l * (1 + s)
    Else
      var_2 = (l + s) - (s * l)
    EndIf
    
    var_1.d = 2 * l - var_2
    
    R = 255 * Hue_2_RGB(var_1, var_2, h + (1.0 / 3.0))
    G = 255 * Hue_2_RGB(var_1, var_2, h)
    B = 255 * Hue_2_RGB(var_1, var_2, h - (1.0 / 3.0))
  EndIf
  
  If alpha=#PB_Ignore
    ProcedureReturn (R | G<<8 | B<<16)
  Else
    ProcedureReturn RGBA(r,g,b,alpha)
  EndIf
  
EndProcedure


clr.i = RGB(Random(255),Random(255),Random(255))
clrt.s = Str(clr)
CheckHSLAMem(clrt)
nclr.i = HSL(hsla_mem(clrt)\hue,hsla_mem(clrt)\sat,hsla_mem(clrt)\lum)
Debug "Original RGB:"
Debug clr
Debug "Converted to HSL and back to RGB:"
Debug nclr
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Post Reply