Page 1 of 1

Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Fri May 17, 2013 9:55 am
by kvitaliy
You can create the illusion of a half-tone transitions and display shades of gray.
Help PureBasic
When an image is saved using palettized depth ( 1, 4 or 8 ), the following flag is available for combination:
#PB_Image_FloydSteinberg: Apply a Floyd-Steinberg dithering.
Result of operation:
Image

Code:

Code: Select all

UseJPEGImageDecoder()
UsePNGImageDecoder()

;{ Windows
Enumeration
  #Window_0
EndEnumeration
;}
;{ Gadgets
Enumeration
  #Image_0
  #Image_1
  #TrackBar_2
  #Button_3
  #Button_4
  #CheckBox_5
EndEnumeration
;}
;{ Images
Enumeration
  #Image_Image_0
  #Image_Image_1
EndEnumeration
;}

Macro Convert()
   If GetGadgetState(#CheckBox_5)
          If IsImage(0) 
             CopyImage(0, 1)
             BlWiImg(1)
             SetGadgetState(#Image_1,  ImageID(1))
          EndIf
          
   Else
          If IsImage(0) 
             CopyImage(0, 1)
             BaWImg(1)
             SetGadgetState(#Image_1,  ImageID(1))
          EndIf
         
   EndIf 
EndMacro

;- black-white
Procedure.l BaWImg(Img)
 
  If Not IsImage(Img) : ProcedureReturn #False : EndIf
  Gree=GetGadgetState(#TrackBar_2)
  ;Debug Gree
  StartDrawing(ImageOutput(Img))
  Width = ImageWidth(Img) - 1
  Height = ImageHeight(Img) - 1
  For y = 0 To Height
    For x = 0 To Width
      Pixel = Point(x, y)
      If   Pixel>RGB(Gree,Gree,Gree) ; 
        Plot(x, y, RGB(255,255,255))
        Else
          Plot(x, y,0)
        EndIf 
      Next x
  Next y
  StopDrawing()

  ProcedureReturn #True
EndProcedure

;-Dithering  Algorithm
Procedure DitherFract(i.i , j.i )
  Static.i DitherError
   
  cc.i = Point(i, j)
  cl.i=cc
    rr.i=Red(cc): gg=Green(cc): bb=Blue(cc)
    cc.i=(2*rr+3*gg+bb)/6 ; Or  (299*rr+587*gg+114*bb)/1000;
    cc_128 = GetGadgetState(#TrackBar_2);
    cc_min = 10
    If cc <= cc_128 
     cc = cc * (110 - cc_min) / cc_128 + cc_min
    Else
      cc = (cc - cc_128) * (255 - 146) / (255 - cc_128) + 146
    EndIf
;         
    cc.i = cc - 128
    
    DitherError = DitherError + cc * 32 / 31
    
    If DitherError <= 0 Or cl<10
      Plot (i, j, 0);RGB(0, 0, 0))
      DitherError = DitherError + 128
    Else
      Plot (i, j, RGB(255, 255, 255))
      DitherError = DitherError - (255 - 128)
      
    EndIf
   
  DitherError = DitherError * 31 / 32
EndProcedure 


Procedure.l BlWiImg(Img)
  StartDrawing(ImageOutput(Img))
  Width = ImageWidth(Img) - 1
  Height = ImageHeight(Img) - 1
  For y = 0 To Height 
     For x = 0 To Width 
        DitherFract(x ,y )
     Next
  Next
   StopDrawing()
EndProcedure

;}
Procedure OpenWindow_Window_0()
  If OpenWindow(#Window_0, 550, 36, 1000, 550, "Converting an RGB image to 1 bit-per-pixel monochrome", #PB_Window_SystemMenu|#PB_Window_MinimizeGadget|#PB_Window_TitleBar)
    ImageGadget(#Image_0, 10, 35, 480, 440, 0, #PB_Image_Border)
    ImageGadget(#Image_1, 505, 35, 480, 440, 0, #PB_Image_Border)
    TrackBarGadget(#TrackBar_2, 505, 490, 480, 25, 1, 255, #PB_TrackBar_Ticks):SetGadgetState(#TrackBar_2,128)
    ButtonGadget(#Button_3, 175, 525, 120, 25, "Save in BMP 1 bpp")
    ButtonGadget(#Button_4, 15, 5, 200, 20, "Load")
    CheckBoxGadget(#CheckBox_5, 875, 515, 155, 25, "Dithering")
  EndIf
EndProcedure

OpenWindow_Window_0()

;{- Event loop
Repeat
  Event = WaitWindowEvent()
  Select Event
    ; ///////////////////
    Case #PB_Event_Gadget
      EventGadget = EventGadget()
      EventType = EventType()
      If EventGadget = #CheckBox_5
        Convert()
      ElseIf EventGadget = #Image_1
      ElseIf EventGadget = #TrackBar_2
        Convert()
      ElseIf EventGadget = #Button_3
        If IsImage(1) 
          File$ = SaveFileRequester("Please choose file to save", "","",0)
          SaveImage(1, File$ , #PB_ImagePlugin_BMP, 0, 1)
        EndIf
      
      ElseIf EventGadget = #Button_4
        Pattern$ = "Image |*.bmp;*.jpg;*.png"
        Filename$ = OpenFileRequester("","",Pattern$,0)
        If Filename$>""
          If  LoadImage(0, Filename$) 
              Repeat
                  If ImageWidth(0)>480 Or ImageHeight(0)>440:ResizeImage(0,ImageWidth(0)/1.1, ImageHeight(0)/1.1):EndIf
              Until ImageHeight(0)<440 And ImageWidth(0)<480
        SetGadgetState(#Image_0,  ImageID(0))
        Convert()
          EndIf
        EndIf
      EndIf
    ; ////////////////////////
    Case #PB_Event_CloseWindow
      EventWindow = EventWindow()
      If EventWindow = #Window_0
        CloseWindow(#Window_0)
        Break
      EndIf
  EndSelect
ForEver
;
;}
Result of operation:
Normal conversion
Image
Dithering Algorithm
Image

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Fri May 17, 2013 12:00 pm
by Joakim Christiansen
Quite cool! :D

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Fri May 17, 2013 12:37 pm
by es_91
Thanks for that.

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Sun May 19, 2013 7:23 pm
by BasicallyPure
After I tried the code from the first post I had an idea for another way to convert images to monochrome.
The 2x2 option calculates the average intensity of a 2x2 block of pixels and converts to 5 different intensity levels.
The 3x3 option calculates the average intensity of a 3x3 block of pixels and converts to 10 different intensity levels.
I changed the code so the size of the original image is preserved.
The saved image is now done with .png format which is much smaller than .bmp.

[Edit] Improved code and added better images.
[Edit2] Added Sierra Lite dithering option provided by Wilbert.
[Edit3] Changes to GUI

Image

Click each thumbnail image to view full sized.
Image Original image; 157 kB.

Image Black & White; 20.3 kB.

Image Dither1; 62.5 kB.

Image Dither2; 54.1 kB.

Image 2x2 default; 28.3 kB.

Image 2x2 randomized; 76.1 kB.

Image 3x3 default; 44.8 kB.

Image 3x3 randomized; 55.3 kB.

Code: Select all

; Convert RGB image to 1 bit monochrome

UseJPEGImageDecoder()
UsePNGImageDecoder()
UsePNGImageEncoder()

#MainWin = 0
#Image_Image_0 = 0
#Image_Image_1 = 1
#White = $FFFFFF
#Black = $000000

;{ Gadgets
Enumeration
  #ImgGad_RGB
  #ImgGad_BW
  #BTN_LOAD
  #BTN_SAVE
  #Trackbar_Cspan
  #TrackBar_Threshold
  #CheckBox_Rnd
  #Option_BW
  #Option_Dither1
  #Option_Dither2
  #Option_Gray5
  #Option_Gray10
  #StatusBar
EndEnumeration
;}

Global sourceFile$, mode = #Option_BW, imgW, imgH

; black-white
Procedure.l BW_Img(Img)
   Protected Gree, Width, Height, x, y, Pixel, cp
   If Not IsImage(Img) : ProcedureReturn #False : EndIf
   Gree = 255 - GetGadgetState(#TrackBar_Threshold)
   
   StartDrawing(ImageOutput(Img))
      Width = ImageWidth(Img) - 1
      Height = ImageHeight(Img) - 1
      For y = 0 To Height
         For x = 0 To Width
            cp = Point(x, y)
            Pixel = (Red(cp)<<1 + Green(cp)<<2 + Blue(cp)) / 7
            If  Pixel > Gree
               Plot(x, y, #White)
            Else
               Plot(x, y, #Black)
            EndIf 
         Next x
      Next y
   StopDrawing()
   
   ProcedureReturn #True
EndProcedure

Procedure Dither1(Img)
   Protected.i cc_128 = 256 - GetGadgetState(#TrackBar_Threshold)
   Protected.i DitherError, cc, cl, rr, gg, bb
   
   StartDrawing(ImageOutput(Img))
      Width = ImageWidth(Img) - 1
      Height = ImageHeight(Img) - 1
      For j = 0 To Height 
         For i = 0 To Width 
            ; Dithering  Algorithm
            cl = Point(i, j)
            cc = (Red(cl)*2 + Green(cl)*3 + Blue(cl)) / 6
            
            If cc <= cc_128 
               cc = cc * 100 / cc_128 + 10
            Else
               cc = (cc - cc_128) * 109 / (255 - cc_128) + 146
            EndIf
            
            cc - 128
            DitherError + cc * 32 / 31
            
            If DitherError <= 0 Or cl < 10
               Plot (i, j, #Black)
               DitherError + 128
            Else
               Plot (i, j, #White)
               DitherError - 127
            EndIf
            
            DitherError * 31 / 32
         Next
      Next
   StopDrawing()
EndProcedure

Procedure Dither2(Img); Sierra Lite
  Protected l, x, y, w = ImageWidth(Img), h = ImageHeight(Img)
  Protected error, errorMul = GetGadgetState(#TrackBar_Threshold)
  Dim errorBuffer(w)
  StartDrawing(ImageOutput(Img))
  While y < h
    x = 0
    error = 0
    While x < w
      error + errorBuffer(x + 1) 
      l = Point(x, y)
      l = (Red(l) * 77 + Green(l) * 150 + Blue(l) * 29) >> 8 + error
      If l >= 128
        Plot(x, y, #White)
        If l > 255
          error = 0
        Else
          error = l - 255
        EndIf
      Else
        Plot(x, y, #Black)
        If l < 0
          error = 0
        Else
          error = l
        EndIf
      EndIf
      error = (error * errorMul) >> 9
      errorBuffer(x + 1) = error >> 1 
      errorBuffer(x) + errorBuffer(x + 1)
      x + 1
    Wend
    y + 1
  Wend
  StopDrawing()
EndProcedure

Procedure.l Gray5(Img) ; by BasicallyPure
   ; convert 2x2 block of pixels into 5 shades of intensity
   
   DataSection
      BP:
      Data.l %00000000, %10001000, %10011001, %11101110, %11111111
   EndDataSection
   
   Protected Width, Height, Pixel, x, y, p, cp
   Protected lum = GetGadgetState(#TrackBar_Threshold) - 128
   Protected Rflag = GetGadgetState(#CheckBox_Rnd)
   
   If IsImage(Img)
      StartDrawing(ImageOutput(Img))
         Width = ImageWidth(Img) - 2
         Height = ImageHeight(Img) - 2
         
         For y = 0 To Height Step 2
            For x = 0 To Width Step 2
               
               Pixel = 0
               
               ; ------- get average intensity for a 2x2 block ----------
               For i = x To x+1
                  For j = y To y+1
                     cp = Point(i,j)
                     Pixel + Red(cp)<<1 + Green(cp)<<2 + Blue(cp)
                  Next j
               Next i
               Pixel / 28 + lum
               ; --------------------------------------------------------
               
               If Pixel  >  255 : Pixel = 255
               ElseIf Pixel < 0 : Pixel = 0
               EndIf
               
               p = PeekL(?BP + (Pixel/52)<<2)
               If Rflag : p >> Random(3) : EndIf
               
               For i = x To x+1
                  For j = y To y+1
                     If p & 1 : Plot(i, j, #White)
                     Else     : Plot(i, j, #Black)
                     EndIf
                     p >> 1
                  Next j
               Next i
               
            Next x
         Next y
         
      StopDrawing()
      ProcedureReturn #True
   EndIf
   
   ProcedureReturn #False
EndProcedure

Procedure.l Gray10(Img) ; by BasicallyPure
   ; convert 3x3 block of pixels into 10 shades of intensity
   
   Protected inc = GetGadgetState(#Trackbar_Cspan)
   Protected Rflag = GetGadgetState(#CheckBox_Rnd)
   Protected lum = GetGadgetState(#TrackBar_Threshold) - 128
   Protected Width, Height, Pixel, x, y, i, j, p, cp
   Protected L1, L2, L3, L4, L5, L6, L7, L8, L9
   
   Macro Plot3x3(pattern)
      p = pattern
      If Rflag : p >> Random(8) : EndIf
      For i = x To x+2
         For j = y To y+2
            If p & 1 : Plot(i,j,#White) : Else : Plot(i,j,#Black) : EndIf
            p >> 1
         Next j
      Next i
   EndMacro
   
   If IsImage(Img)
      L1 = (127 - inc*4) : L2 = (127 - inc*3) : L3 = (127 - inc*2)
      L4 = (127 - inc  ) : L5 = (127        ) : L6 = (127 + inc  )
      L7 = (127 + inc*2) : L8 = (127 + inc*3) : L9 = (127 + inc*4)
      
      StartDrawing(ImageOutput(Img))
         Width = ImageWidth(Img) - 3
         Height = ImageHeight(Img) - 3
         
         For y = 0 To Height Step 3
            For x = 0 To Width Step 3
               
               Pixel = 0
               For i = x To x+2 ; get average intensity for a 3x3 block
                  For j = y To y+2
                     cp = Point(i,j)
                     Pixel + Red(cp)<<1 + Green(cp)<<2 + Blue(cp)
                  Next j
               Next i
               Pixel / 63 + lum
               
               If Pixel  >  255 : Pixel = 255
               ElseIf Pixel < 0 : Pixel = 0
               EndIf
               
               If     Pixel > L9 : Plot3x3(%111111111111111111) ; White
               ElseIf Pixel > L8 : Plot3x3(%111101111111101111) ; Gray1
               ElseIf Pixel > L7 : Plot3x3(%011111110011111110) ; Gray2
               ElseIf Pixel > L6 : Plot3x3(%110011101110011101) ; Gray3
               ElseIf Pixel > L5 : Plot3x3(%101010101101010101) ; Gray4
               ElseIf Pixel > L4 : Plot3x3(%010101010010101010) ; Gray5
               ElseIf Pixel > L3 : Plot3x3(%001010100001010100) ; Gray6
               ElseIf Pixel > L2 : Plot3x3(%100000001100000001) ; Gray7
               ElseIf Pixel > L1 : Plot3x3(%000010000000010000) ; Gray8
               Else              : Plot3x3(%000000000000000000) ; Black
               EndIf
               
            Next x
         Next y
         
      StopDrawing()
      
      ProcedureReturn #True
   EndIf
   
   ProcedureReturn #False
EndProcedure

Procedure Convert()
   If IsImage(0) : CopyImage(0, 1)
      
      Select mode
         Case #Option_BW      : BW_Img(1)
         Case #Option_Dither1 : Dither1(1)
         Case #Option_Dither2 : Dither2(1)
         Case #Option_Gray10  : Gray10(1)
         Case #Option_Gray5   : Gray5(1)
         Default : ProcedureReturn 0
      EndSelect
      
      SetGadgetState(#ImgGad_BW, ImageID(1))
      ProcedureReturn 1
   EndIf
EndProcedure

Procedure INIT_GUI()
   ExamineDesktops()
   DW = DesktopWidth(0)
   flags = #PB_Window_SystemMenu|#PB_Window_MinimizeGadget|#PB_Window_TitleBar
   If OpenWindow(#MainWin, 0, 0, DW, DW/2, "Converting an RGB image to 1 bit-per-pixel monochrome", flags)
      imgW = (WindowWidth(#MainWin) - 30) / 2
      imgH = imgW * 0.75
      CreateStatusBar(#StatusBar, WindowID(#MainWin))
      WH = imgH + 110 + StatusBarHeight(#StatusBar)
      ResizeWindow(#MainWin,0,0,#PB_Ignore,WH)
      SetWindowColor(#MainWin, $D0E0E0)
      AddStatusBarField(#PB_Ignore)
      ImageGadget(#ImgGad_RGB, 10, 35, imgW, imgH, 0, #PB_Image_Border)
      ImageGadget(#ImgGad_BW, imgW+20, 35, imgW, imgH, 0, #PB_Image_Border)
      TrackBarGadget(#TrackBar_Threshold, imgW+20, imgH+45, imgW*0.6, 25, 0, 255)
      SetGadgetState(#TrackBar_Threshold,128)
      TrackBarGadget(#Trackbar_Cspan,imgW*1.6+30,imgH+45,imgW*0.3,25,5,25,#PB_TrackBar_Ticks)
      SetGadgetState(#Trackbar_Cspan,15)
      ButtonGadget(#BTN_LOAD, 10, 5, 100, 25, "Load")
      ButtonGadget(#BTN_SAVE, imgW+20, 5, 100, 25, "Save as PNG")
      OptionGadget(#Option_BW, imgW+20, imgH+80, 50, 25, "B & W")
      SetGadgetState(#Option_BW,#True)
      OptionGadget(#Option_Dither1, imgW+80, imgH+80, 60, 25, "Dither1")
      OptionGadget(#Option_Dither2, imgW+150, imgH+80, 60, 25, "Dither2")
      OptionGadget(#Option_Gray5, imgW+220, imgH+80, 50, 25, "2 x 2")
      OptionGadget(#Option_Gray10, imgW+280, imgH+80, 50, 25, "3 x 3")
      CheckBoxGadget(#CheckBox_Rnd,imgW+340, imgH+80, 80, 25, "Randomize")
   EndIf
EndProcedure

Procedure Save()
   Static Pattern$ = "Image .png|*.png"
   If IsImage(1) And sourceFile$ <> ""
      File$ = SaveFileRequester("Enter filename to save",
      RemoveString(GetFilePart(sourceFile$),"."+GetExtensionPart(sourceFile$))+
      "_B&W.png",Pattern$,0)
      If LoadImage(1,sourceFile$)
         Select mode
            Case #Option_BW      : BW_Img(1)
            Case #Option_Dither1 : Dither1(1)
            Case #Option_Dither2 : Dither2(1)
            Case #Option_Gray10  : Gray10(1)
            Case #Option_Gray5   : Gray5(1)
            Default : ProcedureReturn 0
         EndSelect
         SaveImage(1, File$ , #PB_ImagePlugin_PNG, 0, 1)
         Convert()
      EndIf
   EndIf
EndProcedure

Procedure Load()
   Static Pattern$ = "Image .bmp, .jpg, .png|*.bmp;*.jpg;*.png|All files (*.*)|*.*"

   Protected w, h, Ratio.f
   sourceFile$ = OpenFileRequester("", "", Pattern$, 0)
   
   If sourceFile$ > ""
      If  LoadImage(0, sourceFile$)
         StatusBarText(#StatusBar, 0, sourceFile$)
         w = ImageWidth(0) : h = ImageHeight(0)
         If w > imgW Or h > imgH
            Ratio = h / w
            
            If Ratio < 0.75 ;size by width
               Ratio = imgW / w
            Else ; size by height
               Ratio = imgH / h
            EndIf
            
            ResizeImage(0, w * Ratio, h * Ratio)
         EndIf
         
         SetGadgetState(#ImgGad_RGB, ImageID(0))
         Convert()
      EndIf
   EndIf
EndProcedure

INIT_GUI()

;- Event loop
;{
Repeat
   Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
         Break
      Case #PB_Event_Gadget : nGadget = EventGadget()
         Select nGadget
            Case #Trackbar_Cspan To #Option_Gray10
               If nGadget >= #Option_BW : mode = nGadget : EndIf
               Convert()
            Case #BTN_SAVE : Save()
            Case #BTN_LOAD : Load()
         EndSelect
   EndSelect
ForEver
;}
BP

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Mon May 20, 2013 3:15 am
by BasicallyPure
Improved code.
Dither is now much faster.
Better luminance detection for 2x2 and 3x3.
Made randomize an option for both 2x2 and 3x3.
Added better example images.

Randomize is good to use if you plan to display the monochrome images at sizes different from the original.

I just noticed that Internet Explorer is not displaying these images in the best possible way.
Try downloading one of the large monochrome images (right click and save as...) then view it in a good photo viewer.
Maybe there is a setting somewhere in IE to fix this but I can't find it.

BP

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Mon May 20, 2013 4:31 am
by kvitaliy
Thanks for the interesting code improvement!

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Mon May 20, 2013 8:06 am
by wilbert
A different dithering algorithm

Code: Select all

Procedure Dither(Img); Sierra Lite
  Protected l, x, y, w = ImageWidth(Img), h = ImageHeight(Img)
  Protected error, errorMul = GetGadgetState(#TrackBar_Threshold)
  Dim errorBuffer(w)
  StartDrawing(ImageOutput(Img))
  While y < h
    x = 0
    error = 0
    While x < w
      error + errorBuffer(x + 1) 
      l = Point(x, y)
      l = (Red(l) * 77 + Green(l) * 150 + Blue(l) * 29) >> 8 + error
      If l >= 128
        Plot(x, y, #White)
        If l > 255
          error = 0
        Else
          error = l - 255
        EndIf
      Else
        Plot(x, y, #Black)
        If l < 0
          error = 0
        Else
          error = l
        EndIf
      EndIf
      error = (error * errorMul) >> 9
      errorBuffer(x + 1) = error >> 1 
      errorBuffer(x) + errorBuffer(x + 1)
      x + 1
    Wend
    y + 1
  Wend
  StopDrawing()
EndProcedure

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Mon May 20, 2013 6:04 pm
by BasicallyPure
Thanks Wilbert,
I have added the 'Sierra Lite' dithering algorithm to the code as option 'dither2'.
I think it gives the best results of all the options.

BP

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Mon May 20, 2013 6:32 pm
by davido
Thanks BP

Very nice :D

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Tue May 21, 2013 11:04 am
by Shield
Pretty interesting code and good results! Thank you. :)

Re: Converting an RGB image to 1 bit-per-pixel monochrome

Posted: Fri May 24, 2013 9:09 pm
by Kwai chang caine
Very useful for me in this time
Thanks a lot kvitaliy works great 8)