Dithering - forward-array vs direct modification

Just starting out? Need help? Post your questions and find answers here.
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Dithering - forward-array vs direct modification

Post by Keya »

hello! this week i am trying to learn dithering! I cant help but find it interesting how the pixel error is propagated over neighboring pixels to share the load/spread the burden, and I was pleasantly surprised how relatively easy it is... BUT there's a couple specific details I need clarification on! (It seems it's easy to get a result that's nearly correct but not quite... so I'm not sure how to verify my implementation!)

My main source of confusion is whether or not to 1) use a forward array to hold the next rows error values (only modifying the current pixel), OR 2) directly modify the pixels in the next row as I go (5 pixels modified per pixel analyzed for Floyd-Steinberg)?

The reason I'm confused is because general text readings seem to suggest the need for an array (simply the size of 1 row and usually plus one or two extra). BUT so many source codes ive seen seem to directly modify the forward pixels, and i'm not sure but that seems incorrect because if you go 0<>255 you lose the propagated error information? hence the need to store in forward array? So im wondering if perhaps those direct-mod implementations are actually incorrect, even though they still result in what looks like a valid dither

Anyway here's my attempt at both types of implementation, and sure enough they produce different results (although im not 100% sure both implementations are correct!). Im not concerned about speed, just readability as i just want to get my head around it, so Plot() is my friend lol :)

I would really appreciate it if anyone with experience in this field could help clarify this, and check my implementations :)

The forward-array version is based on the source code example at https://omohundro.files.wordpress.com/2 ... hering.pdf which is one of the few examples i could find that actually uses a forward array.

[edit]Disregard the Array version of this Floyd-Steinberg test, the implementation is wrong. Scroll down for correct Array versions using Sierra Lite and JJN[/edit]

Code: Select all

#PBBlack = $000000
#PBWhite = $FFFFFF

Procedure.i TruncF0255 (a.f)
  If a > 255: a = 255: ElseIf a < 0: a = 0: EndIf
  ProcedureReturn a
EndProcedure


Procedure RGBtoLum(c.l)  ;Get luminosity (the min and max RGB levels / 2)
  r = (c & $FF) :  g = ((c & $FF00) >> 8) :  b = ((c & $FF0000) >> 16) 
  If g < r: min = g: Else: min = r: EndIf
  If b < min: min = b: EndIf  
  If g > r: max = g: Else: max = r: EndIf
  If b > max: max = b: EndIf   
  lum = (max + min) >> 1  ;/ 2.0
  ProcedureReturn lum
EndProcedure



;Floyd-Steinberg:
;         *   7            =               *    0.4375
;     3   5   1   (1/16)        0.1875  0.3125  0.0625


Procedure DoFloydARRAY(hImg)
  Protected nextpixel.f, ierror.f, oldpixel.f, lr.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  Dim xline.f(width+1)
  For y = 2 To height-3     ;completely avoid edges for this test just to keep things easy
    lr = 0
    For x = 2 To width-3
      oldpixel = Truncf0255(RGBtoLum(Point(x,y)) + xline(x)) 
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #PBBlack)
      Else
        newpixel = 255
        Plot(x,y, #PBWhite)
      EndIf
      quant_error.f = oldpixel - newpixel           
      xline(x-1) + (0.1875 * quant_error)
      xline(x)   = (0.3125 * quant_error) + lr
      xline(x+1) + (0.0625 * quant_error)
      lr = (0.4375 * quant_error)
    Next x
  Next y
  StopDrawing()
EndProcedure



Procedure DoFloydDIRECT(hImg)
  Protected nextpixel.f, ierror.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  For y = 2 To height-3            ;completely avoid edges for this test just to keep things easy
    For x = 2 To width-3
      oldpixel = RGBtoLum(Point(x,y))
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #PBBlack)
      Else
        newpixel = 255
        Plot(x,y, #PBWhite)
      EndIf
      quant_error.f = oldpixel - newpixel
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y)) + (0.4375 * quant_error))
      Plot(x+1,y, RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x-1,y+1)) + (0.1875 * quant_error))
      Plot(x-1,y+1, RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x,y+1)) + (0.3125 * quant_error))
      Plot(x,y+1,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y+1)) + (0.0625 * quant_error))
      Plot(x+1,y+1, RGB(newx,newx,newx))
    Next x
  Next y
  StopDrawing()
EndProcedure




UsePNGImageDecoder()
UseJPEGImageDecoder()
If LoadImage(0, "C:\temp\any.jpg") = 0
  MessageRequester("Fail","Loadimage failed"): End
EndIf
If OpenWindow(0, 0, 0, ImageWidth(0)*2, ImageHeight(0)+10, "Floyd-Steinberg Test", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
  hCopy = CopyImage(0,#PB_Any) 
  
  DoFloydARRAY(hCopy)     ;v1 - use forward array, only modify current pixel
  ;DoFloydDIRECT(hCopy)   ;v2 - directly modify forward pixels
  
  ImageGadget(0, 0, 0, 200, 200, ImageID(0))  
  ImageGadget(1, ImageWidth(0), 0, 200, 200, ImageID(hCopy))
  Repeat
    Event = WaitWindowEvent()
  Until Event = #PB_Event_CloseWindow
EndIf
Last edited by Keya on Tue Jan 26, 2016 4:30 pm, edited 6 times in total.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Floyd-Steinberg dither

Post by wilbert »

Very interesting.
My personal favorite is the Sierra Lite algorithm which is a bit similar but as far as I can remember, I always used a one line buffer.
I haven't seen before the approach of modifying directly without a buffer.
From the two procedures you created, the DoFloydDIRECT has a better looking result I think.
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
JHPJHP
Addict
Addict
Posts: 2265
Joined: Sat Oct 09, 2010 3:47 am

Re: Floyd-Steinberg dither

Post by JHPJHP »

Hi Keya,
(It seems it's easy to get a result that's nearly correct but not quite... so I'm not sure how to verify my implementation!)
PureBasic Interface to OpenCV should help verify a correct implementation for this and numerous other areas of image manipulation.

Even though the interface is Windows only most the algorithms aren’t restricted by platform or even OpenCV for that matter.
Last edited by JHPJHP on Mon Jan 25, 2016 8:00 pm, edited 1 time in total.

If you're not investing in yourself, you're falling behind.

My PureBasic StuffFREE STUFF, Scripts & Programs.
My PureBasic Forum ➤ Questions, Requests & Comments.
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Floyd-Steinberg dither

Post by Keya »

wilbert here is luis' amazing quantize code which you made an amazing SSE version from heehee :)
http://www.purebasic.fr/english/viewtop ... 12&t=39606
here is the macro he uses for Floyd-Steinberg, i might be mistaken but it too seems to directly modify the pixels:

Code: Select all

Macro FLOYD_STEINBERG (col, soft)
 ; Floyd-Steinberg filter
 ;     *   7
 ; 3   5   1  (1/16)

 ; position inside the palette indexed by iIndex (0..255)
 *pal = *Palette + (iIndex << 2) + col
 
 ; difference from the current pixel's color inside the source image
 ; and the nearest one inside the palette
 iError = (*ptr\Byte[col] - *pal\a) >> soft
 
  GetPixel32BitImage (x + 1, y, *ptr)
  ADD_CLIP_0_255 (*ptr\Byte[col], (iError * 7) >> 4) ; 7/16

  GetPixel32BitImage (x - 1, y + 1, *ptr)
  ADD_CLIP_0_255 (*ptr\Byte[col], (iError << 1 + iError) >> 4) ; 3/16
    
  GetPixel32BitImage (x, y + 1, *ptr)
  ADD_CLIP_0_255 (*ptr\Byte[col], (iError << 2 + iError) >> 4) ; 5/16
    
  GetPixel32BitImage (x + 1, y + 1, *ptr)
  ADD_CLIP_0_255 (*ptr\Byte[col], (iError) >> 4) ; 1/16
EndMacro
JHPJHP ive seen the OpenCV thread pop up numerous times, i guess now is as good a time as any to check it out, thanks :)
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Floyd-Steinberg dither

Post by wilbert »

Keya wrote:here is the macro he uses for Floyd-Steinberg, i might be mistaken but it too seems to directly modify the pixels
It indeed looks that way :)
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Floyd-Steinberg dither

Post by Keya »

Here is my version of the one from the PDF i linked in the OP, no algorithm name was given but the paper's author is Omohundro.
I'm not 100% sure of the kernel but it seems to be [- * 1, 1 3 3], judging from the source comments (and I then went on to modify this to create the Floyd-Steinberg version posted above, as the kernel seems the same shape). Strange the kernel isnt Floyd-Steinberg though seeing as that's what the paper is about:

Code: Select all

;            *    1               
;        1   3    3       (1/8)       <- my best guess
Procedure DoOmohundro(hImg)
  Protected nextpixel.f, ierror.f, fer.f, eer.f, teer.f, lr.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)  
  Protected error.f         ;hold error for current pixel
  Dim error_arr.f(width)  
  For y = 2 To height-3
    lr = 0
    For x = 2 To width-3      
      val = TruncF0255(RGBtoLum(Point(x,y)) + error_arr(x)) 
      If val > 128        
        Plot(x,y,#White)     
        error = val-255
      Else
        Plot(x,y,#Black) 
        error = val
      EndIf            
      fer.f = error/4     ;a fourth of the error
      eer.f = fer/2       ;an eighth
      teer.f = fer + eer  ;three eights
      error_arr(x-1) + eer
      error_arr(x) = teer + lr
      lr = eer
      error_arr(x+1) + teer
    Next x
  Next y
  StopDrawing()
EndProcedure
Last edited by Keya on Mon Jan 25, 2016 8:29 pm, edited 1 time in total.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Floyd-Steinberg dither

Post by wilbert »

This might also be a nice page to look at
http://www.tannerhelland.com/4660/dithe ... urce-code/
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Floyd-Steinberg dither

Post by Keya »

yes thats awesome page!!! i've got a few others too ive read intensively over the past few days but cant find them amongst the 160 other open browser panels lol

Here is a 1D kernel (also based on code from the PDF, but different code to the one above), which i found a nice introduction. Ideal for Sundays! Even being 1D it still uses an array and directly modifies only the current pixel

Code: Select all

; 1    *     1    1       (1/8)    <- looking at this it doesnt seem to propagate the full error (like Atkinson), but looking at the code it does add "+3" twice, so maybe it does. And interestingly it modifies a PRIOR pixel! Anyway im confused!
Procedure Do1D(hImg)
  Protected nextpixel.f, ierror.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg) 
  Protected error.f         ;hold error for current pixel
  Dim error_arr.f(width)  
  For y = 2 To height-3
    For x = 2 To width-3
      val = RGBtoLum(Point(x,y)) + error_arr(x+1)
      If val > 128
        Plot(x,y,#White)       ;
        error = val-255
      Else
        Plot(x,y,#Black)
        error = val
      EndIf      
      error_arr(x-1) = error_arr(x-1)+(error/8)
      error_arr(x)   = error_arr(x)+3*(error/8)
      error_arr(x+1) = (error/8)
      error_arr(x+2) = error_arr(x+2)+3*(error/8)
    Next x
  Next y
  StopDrawing()
EndProcedure
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

SIERRA LITE

Post by Keya »

please forget about the array-based Floyd-Steinberg test in the first post, theres an error in the implementation.

Anyway to help get my head around dithering ive switched to the Sierra Lite kernel due to its simplicity and classroom friendliness :D

I recoded the array version so it uses a full (x,y) error array - one for every pixel. Inefficient, but clean, simple and higher confidence!

Sample copy (orig can be found here):
Image
Here is Sierra Lite DIRECT vs ARRAY:
Image
Direct: B=44817 W=176299
Array: B=44645 W=176471

So it seems the Direct method is losing some error information 0<>255 (or the results would be identical? but then, the results are so close that maybe information isnt lost but passed on like overflow? but perhaps because im ignoring edges the feedback is affected?), but it seems to balance out on both sides so the result is still close enough to zero? And perhaps in some or perhaps even most situations that's good enough? And because its a tiny kernel perhaps it all balances out in that many pixels? (a larger kernel might show more disparities? i will try the large JJN next!) Im just thinking out loud here! I think i feel more comfortable with the Array method though seeing as it seems the truer version by retaining the errors in full (but then, Atkinson kernel only propagates 75% of an error!), and the texts ive read seem to suggest that's the correct way, even if so many source codes ive seen use the Direct method. But then, the Direct method seems a bit easier especially for larger kernels, doesnt require any additional vars/array, and possibly a bit faster too, for a result that a Sierra Lite sniffer dog probably couldnt tell apart, so perhaps they're both equally valid approaches? Fascinating, lol

Here is the Sierra Lite version of Direct vs Array:

Code: Select all

#PBBlack = $000000:   #PBWhite = $FFFFFF

Procedure.i TruncF0255 (a.f)
  If a > 255: a = 255: ElseIf a < 0: a = 0: EndIf
  ProcedureReturn a
EndProcedure


Procedure RGBtoLum(c.l)  ;Get luminosity (the min and max RGB levels / 2)
  r = (c & $FF) :  g = ((c & $FF00) >> 8) :  b = ((c & $FF0000) >> 16) 
  If g < r: min = g: Else: min = r: EndIf
  If b < min: min = b: EndIf  
  If g > r: max = g: Else: max = r: EndIf
  If b > max: max = b: EndIf   
  lum = (max + min) >> 1  ;/ 2.0
  ProcedureReturn lum
EndProcedure



;Sierra-2-4A aka Sierra Lite     
;         *   2           =            *     0.5
;     1   1     (1/4)          0.25  0.25

Procedure SierraLiteDIRECT(hImg)
  Protected nextpixel.f, ierror.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  For y = 2 To height-3                     ;completely avoid the edges to keep it simple for this test
    For x = 2 To width-3
      oldpixel = RGBtoLum(Point(x,y))
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #Black)
      Else
        newpixel = 255
        Plot(x,y, #White)
      EndIf
      quant_error.f = oldpixel - newpixel
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y))   + (0.5  * quant_error))  ;2/4
      Plot(x+1,y, RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x-1,y+1)) + (0.25 * quant_error))  ;1/4
      Plot(x-1,y+1, RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x,y+1))   + (0.25 * quant_error))  ;1/4
      Plot(x,y+1,  RGB(newx,newx,newx))
    Next x
  Next y
  StopDrawing()
EndProcedure


Procedure SierraLiteARRAY(hImg)
  Protected nextpixel.f, oldpixel.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  Dim error(width+5,height+5)  ;An error(x,y) for every pixel
  For y = 2 To height-3                     ;completely avoid the edges to keep it simple for this test
    For x = 2 To width-3
      oldpixel = Truncf0255(RGBtoLum(Point(x,y)) + error(x,y)) 
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #PBBlack)
      Else
        newpixel = 255
        Plot(x,y, #PBWhite)
      EndIf      
      quant_error.f = oldpixel - newpixel
      error(x+1,y)   + (0.5  * quant_error)  ;2/4
      error(x-1,y+1) + (0.25 * quant_error)  ;1/4
      error(x,y+1)   + (0.25 * quant_error)  ;1/4
    Next x
  Next y
  StopDrawing()
EndProcedure


Procedure CountBW(hImg)
  StartDrawing(ImageOutput(hImg))
  For y = 0 To ImageHeight(hImg)-1
    For x = 0 To ImageWidth(hImg)-1
      If Point(x,y) = 0: B + 1: Else: W + 1: EndIf
    Next x
  Next y
  StopDrawing()
  Debug("Black=" + Str(B) + "  White=" + Str(W))
EndProcedure


UsePNGImageDecoder():  UseJPEGImageDecoder()
If LoadImage(0, "C:\temp\Cube.jpg") = 0
  MessageRequester("Fail","Loadimage failed"): End
EndIf
If OpenWindow(0, 0, 0, ImageWidth(0)*2, ImageHeight(0)+10, "Sierra Lite Test", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  hCopy = CopyImage(0,#PB_Any) 
  
  SierraLiteDIRECT(hCopy)
  ;SierraLiteARRAY(hCopy)
  
  hTrim = GrabImage(hCopy, #PB_Any, 2,2,ImageWidth(0)-4,ImageHeight(0)-4)  ;for simplicity we're ignoring edges for this test
  CountBW(hTrim)
  ;SaveImage(hTrim,"c:\temp\CubeDirect.bmp")
  
  ImageGadget(0, 0, 0, 200, 200, ImageID(0))  
  ImageGadget(1, ImageWidth(0), 0, 200, 200, ImageID(hTrim))
  Repeat
    Event = WaitWindowEvent()
  Until Event = #PB_Event_CloseWindow
EndIf
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: SIERRA LITE

Post by wilbert »

Nice result :)
Keya wrote:Anyway to help get my head around dithering ive switched to the Sierra Lite kernel due to its simplicity and classroom friendliness :D
It's also faster and easier to convert to asm because of it using powers of two.
Keya wrote:But then, the Direct method seems a bit easier especially for larger kernels, doesnt require any additional vars/array, and possibly a bit faster too
It seems to me the array method might be faster since you need to read and write image pixels less often. :?
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Jarvis-Judice-Ninke

Post by Keya »

because Sierra Lite is a tiny kernel I wondered if a large kernel would make Array vs Direct more obvious,
so I tried with Jarvis-Judice-Ninke ...
Image
Direct: B=43257 W=177859
Array: B=43416 W=177700
This is surely a THUMBS UP for the "error-lossless" Array method!! Or rather, more a Thumbs Down for the Direct method which seems to have more patterny artifacts in that clear center compared to the nicer spread from Array

Code: Select all

;Jarvis-Judice-Ninke:
;             X   7   5         
;     3   5   7   5   3   (1/48)
;     1   3   5   3   1 
 
Procedure JarvisJudiceNinkeDIRECT(hImg)
  Protected nextpixel.f, ierror.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  For y = 3 To height-4
    For x = 3 To width-4
      oldpixel = RGBtoLum(Point(x,y))
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #Black)
      Else
        newpixel = 255
        Plot(x,y, #White)
      EndIf
      quant_error.f = oldpixel - newpixel
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y))     + ((7/48) * quant_error))
      Plot(x+1,y, RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+2,y))     + ((5/48) * quant_error))
      Plot(x+2,y, RGB(newx,newx,newx))
      
      newx.f = TruncF0255(RGBtoLum(Point(x-2,y+1))   + ((3/48) * quant_error))
      Plot(x-2,y+1,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x-1,y+1))   + ((5/48) * quant_error))
      Plot(x-1,y+1,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x,  y+1))   + ((7/48) * quant_error))
      Plot(x,  y+1,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y+1))   + ((5/48) * quant_error))
      Plot(x+1,y+1,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+2,y+1))   + ((3/48) * quant_error))
      Plot(x+2,y+1,  RGB(newx,newx,newx))
      
      newx.f = TruncF0255(RGBtoLum(Point(x-2,y+2))   + ((1/48) * quant_error))
      Plot(x-2,y+2,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x-1,y+2))   + ((3/48) * quant_error))
      Plot(x-1,y+2,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x,  y+2))   + ((5/48) * quant_error))
      Plot(x,  y+2,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+1,y+2))   + ((3/48) * quant_error))
      Plot(x+1,y+2,  RGB(newx,newx,newx))
      newx.f = TruncF0255(RGBtoLum(Point(x+2,y+2))   + ((1/48) * quant_error))
      Plot(x+2,y+2,  RGB(newx,newx,newx))      
    Next x
  Next y
  StopDrawing()
EndProcedure


Procedure JarvisJudiceNinkeARRAY(hImg)
  Protected nextpixel.f, oldpixel.f
  StartDrawing(ImageOutput(hImg))
  width = ImageWidth(hImg): height = ImageHeight(hImg)
  Dim error.f(width+5,height+5)  ;An error(x,y) for every pixel
  For y = 3 To height-4
    For x = 3 To width-4
      oldpixel = Truncf0255(RGBtoLum(Point(x,y)) + error(x,y)) 
      If oldpixel < 128
        newpixel = 0
        Plot(x,y, #PBBlack)
      Else
        newpixel = 255
        Plot(x,y, #PBWhite)
      EndIf      
      quant_error.f = oldpixel - newpixel
      error(x+1,y)   + ((7/48) * quant_error)
      error(x+2,y)   + ((5/48) * quant_error)
      
      error(x-2,y+1) + ((3/48) * quant_error)
      error(x-1,y+1) + ((5/48) * quant_error)
      error(x,  y+1) + ((7/48) * quant_error)
      error(x+1,y+1) + ((5/48) * quant_error)
      error(x+2,y+1) + ((3/48) * quant_error)
      
      error(x-2,y+2) + ((1/48) * quant_error)
      error(x-1,y+2) + ((3/48) * quant_error)
      error(x,  y+2) + ((5/48) * quant_error)
      error(x+1,y+2) + ((3/48) * quant_error)
      error(x+2,y+2) + ((1/48) * quant_error)      
    Next x
  Next y
  StopDrawing()
EndProcedure
Last edited by Keya on Tue Jan 26, 2016 4:24 pm, edited 2 times in total.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Jarvis-Judice-Ninke

Post by wilbert »

Keya wrote:This is clearly a THUMBS UP for the "truer" Array method!! The Direct method seems to have more artifacts in that clear center
Your array method stores a floating point error which is more accurate compared to the integer values of the direct method.
So when it comes to accuracy, your floating point array performs best :)
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Floyd-Steinberg dither

Post by Keya »

Thanks! Makes sense. I tried to keep everything Float in both but I guess because Direct has to store integer 0-255 immediately it loses that extra Float precision ... which i guess kinda helps explain why the result is nearly as good as Array, especially with small kernel where the pixels are only overwritten 3 times in the case of Sierra Lite vs (2x5)+2 times with JJN! But I think i will sleep better at night if I use Array for all kernels, there's too much iffyness unsettling my mind with Direct, despite its nearly-identical results, and, well... might as well use the best and true one! :) (even though so many source codes ive seen use Direct!)
Precision FTW! lol
User avatar
Keya
Addict
Addict
Posts: 1890
Joined: Thu Jun 04, 2015 7:10 am

Re: Dithering - forward-array vs direct modification

Post by Keya »

Everyone on the planet seems to have their own subtle variation on the implementation, lol. I wish there were test vector image examples!
I'm able to get the famous checkerboard pattern (well, they say checkers i say chess) of Floyd-Steinberg from a 50% gray image no problems (seems one of the few ways to actually test!), but I can't get GIMP to produce a good result. Im just using Image -> Indexed... to change to 2-color, with F-S dithering selected, and the result is a bit messy...
Image
it's as if it's changed the gray to various other colors other than the original $808080, but obviously it hasnt just changed it to black or the whole image would be black, so im not sure what its doing but it doesn't seem right, although you can tell from the first ~2 rows it at least started out ok before it quickly fell over. No idea if it's Array or Direct... if its Direct perhaps thats the problem!
Anyway that's GIMPs problem not mine heehee :D
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3943
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Dithering - forward-array vs direct modification

Post by wilbert »

Keya wrote:although you can tell from the first ~2 rows it at least started out ok before it quickly fell over.
Strange. Maybe GIMP has a different formula to compute the intensity of a pixel like correcting for screen gamma or something like that. :?
Windows (x64)
Raspberry Pi OS (Arm64)
Post Reply