Dithering

Keith Vetter 2006-07-22 : is a technique used in computer graphics to create the illusion of color depth in images with a limited color palette (color quantization). In a dithered image, colors not available in the palette are approximated by a diffusion of colored pixels from within the available palette. [L1 ]

Here some code that implements some dithering algorithms, along with the requisite demo code. The code here is only for gray-scaled images but could easily be extended to color images. Also, another improvement would be to use an optimized palette before dithering (see Reduce Colour Depth - Median Cut).

This was a really fun project. To save space in the wiki, I didn't include a sample image in the demo. Instead, I tried linking to tcl demo images and images from the web (I included some famous image processing images such as Lena).

For fun, try out some of the various web images I included links for and see a) how many colors are needed by the various algorithms to look good, and b) how badly some algorithms are with certain images at different color depths.

For example, try the tcl demo code image of the teapot at just 2 colors and then 3 colors.


 ##+##########################################################################
 #
 # dither.tcl -- Plays with various types of dithering
 # by Keith Vetter, May 2006
 #
 # image source: http://sipi.usc.edu/database/database.cgi?volume=misc
 # dithering overview: http://www.visgraf.impa.br/Courses/ip00/proj/Dithering1/algoritmos_desenvolvidos.htm
 #

 package require Tk
 package require http
 package require Img
 if {! [catch {package require tile 0.7.2}]} {   ;# Use tile if present
    namespace import -force ::ttk::button
 }

 set S(title) "Dithering"
 set S(numShades) 2
 set S(status) blank

 set lenaURL http://www.visgraf.impa.br/Courses/ip00/proj/Dithering1/image/lena.gif
 set teapotImg [file join $tk_library demos images teapot.ppm]

 set images {}
 lappend images [list Lena $lenaURL]
 lappend images [list "Lena (full size)" http://sipi.usc.edu/services/database/misc/4.2.04.tiff]
 lappend images [list "Mandrill" http://sipi.usc.edu/services/database/misc/4.2.03.tiff]
 lappend images [list "Sailboat on lake" http://sipi.usc.edu/services/database/misc/4.2.06.tiff]
 lappend images [list "Elaine" http://sipi.usc.edu/services/database/misc/elaine.512.tiff]
 lappend images [list "Gray Scale" http://www.sput.nl/images/grey2.gif]
 lappend images [list "Gray Scale 2" http://support.sas.com/techsup/technote/ts688/gray.gif]

 lappend images {- -}
 lappend images [list Teapot $teapotImg]
 lappend images [list Earth [file join $tk_library demos images earth.gif]]
 lappend images [list "Active Tcl Splash" [file join $tk_library images activetclsplash.gif]]
 lappend images [list "Bliss Wallpaper" [file join $env(windir) Web/Wallpaper/Bliss.bmp]]

 proc Floyd-Steinberg {numShades srcImg dstImg} {
    set iw [image width $srcImg]
    set ih [image height $srcImg]
    set factor [expr {($numShades - 1) / 255.0}];# For computing output color

    # Error matrix, be lazy and over allocate
    for {set x -1} {$x <= $iw} {incr x} {       ;# NB. extend beyond boundary
        for {set y -1} {$y <= $ih} {incr y} {
            set cerror($x,$y) 0
        }
    }
    for {set y 0} {$y < $ih} {incr y} {
        set y2 [expr {$y + 1}]
        set data [$srcImg data -from 0 $y $iw $y2] ;# Read whole row of image

        set FSData {}
        set direction [expr {($y & 1) ? -1 : 1}];# Serpentine scan-line access
        for {set idx 0} {$idx < $iw} {incr idx} {
            set x [expr {$direction ? $idx : $iw-1-$idx}]
            set x2 [expr {$x + $direction}]
            set x0 [expr {$x - $direction}]

            set pxl "0x[string range [lindex $data 0 $x] 1 2]" ;# Pixel color
            set src [expr {$pxl + $cerror($x,$y)}] ;# With error added in
            set dst [expr {round(floor($src * $factor + .5) / $factor)}]
            set dst [expr {$dst < 0 ? 0 : $dst > 255 ? 255 : $dst}]

            lappend FSData [format "\#%02X%02X%02X" $dst $dst $dst]

            set err [expr {$src - $dst}]
            set cerror($x2,$y) [expr {$cerror($x2,$y) + $err*7/16.0}]
            set cerror($x2,$y2) [expr {$cerror($x2,$y2) + $err*1/16.0}]
            set cerror($x,$y2) [expr {$cerror($x,$y2) + $err*5/16.0}]
            set cerror($x0,$y2) [expr {$cerror($x0,$y2) + $err*3/16.0}]
        }
        $dstImg put [list $FSData] -to 0 $y $iw $y2
        update
    }
 }
 proc AverageDither {numShades srcImg dstImg} {
    set iw [image width $srcImg]
    set ih [image height $srcImg]
    set factor [expr {($numShades - 1) / 255.0}];# For computing output color

    for {set y 0} {$y < $ih} {incr y} {
        set y2 [expr {$y + 1}]
        set data [$srcImg data -from 0 $y $iw $y2]

        set ddata {}
        for {set x 0} {$x < $iw} {incr x} {
            set pxl "0x[string range [lindex $data 0 $x] 1 2]" ;# Pixel color
            set dst [expr {round(floor($pxl * $factor + .5) / $factor)}]
            set dst [expr {$dst < 0 ? 0 : $dst > 255 ? 255 : $dst}]
            lappend ddata [format "\#%02X%02X%02X" $dst $dst $dst]
        }
        $dstImg put [list $ddata] -to 0 $y $iw $y2
        update
    }
 }
 proc OrderedDither {numShades srcImg dstImg} {
    set iw [image width $srcImg]
    set ih [image height $srcImg]

    set omatrix {
        {1  9  3 11}
        {13  5 15  7}
        {4 12  2 10}
        {16  8 14  6}}

    # We're scanning row by row instead of 4x4 chunks because
    # speed is not important here and this way has no corner cases
    for {set y 0} {$y < $ih} {incr y} {
        set mrow [expr {$y % 4}]                ;# Row in ordering matrix
        set y2 [expr {$y + 1}]
        set data [$srcImg data -from 0 $y $iw $y2]

        set ddata {}
        for {set x 0} {$x < $iw} {incr x} {
            set pxl "0x[string range [lindex $data 0 $x] 1 2]" ;# Pixel color
            set pxl [expr {round ($pxl * 16 / 255.0)}]
            set threshold [lindex $omatrix $mrow [expr {$x % 4}]]
            set dst [expr {$pxl < $threshold ? 0 : 255}]
            lappend ddata [format "\#%02X%02X%02X" $dst $dst $dst]
        }
        $dstImg put [list $ddata] -to 0 $y $iw $y2
        update
    }
 }
 proc RandomDither {numShades srcImg dstImg} {
    set iw [image width $srcImg]
    set ih [image height $srcImg]

    for {set y 0} {$y < $ih} {incr y} {
        set y2 [expr {$y + 1}]
        set data [$srcImg data -from 0 $y $iw $y2]

        set ddata {}
        for {set x 0} {$x < $iw} {incr x} {
            set pxl "0x[string range [lindex $data 0 $x] 1 2]" ;# Pixel color
            set dst [expr {$pxl > (rand()*255) ? 255 : 0}]
            lappend ddata [format "\#%02X%02X%02X" $dst $dst $dst]
        }
        $dstImg put [list $ddata] -to 0 $y $iw $y2
        update
    }
 }

 # DEMO CODE
 proc DoDisplay {} {
    if {! [winfo exists .c]} {
        wm title . $::S(title)
        wm protocol . WM_DELETE_WINDOW exit
        wm geom . +10+10
        bind all <Key-F2> {console show}

        canvas .c -xscrollcommand {.sb set} -bd 0 -highlightthickness 0
        scrollbar .sb -orient horizontal -command {.c xview}
        pack .c -side top -fill both -expand 1
        pack .sb -side bottom -fill x
        DoMenus
    }
    MakeFrame .f1 src Original 2
    MakeFrame .f2 avg "Average Dither" 1
    MakeFrame .f3 fs "Floyd-Steinberg" 1
    MakeFrame .f4 rnd "Random Dither" 1
    MakeFrame .f5 ord "Ordered Dither"
    update idletasks
    set x 0
    set h 0
    foreach child {.f1 .f2 .f3 .f4 .f5} {
        .c create window $x 0 -tag $child -window $child -anchor nw
        incr x [winfo reqwidth $child]
        if {[winfo reqheight $child] > $h} {set h [winfo reqheight $child]}
    }
    set maxWidth [expr {[winfo screenwidth .] - 50}]
    set width [expr {$x > $maxWidth ? $maxWidth : $x}]
    .c config -width $width -height $h -scrollregion [list 0 0 $x $h]

    set ::S(colors,src) "[CountColors ::img::src] colors"
 }
 proc DoMenus {} {
    menu .m
    . configure -menu .m
    .m add cascade -menu .m.file -label File -underline 0
    menu .m.file -tearoff 0
    .m.file add command -label "Open File" -under 5 -command DoOpen
    .m.file add command -label "Open Web" -under 5 -command OpenWeb -state disabled
    .m.file add separator
    .m.file add command -label "Exit" -under 0 -command exit

    .m add cascade -menu .m.imgs -label Images -underline 0
    menu .m.imgs -tearoff 0

    .m add cascade -menu .m.help -label Help -underline 0
    menu .m.help -tearoff 0
    .m.help add command -label "About" -underline 0 -command Help

    foreach item $::images {
        foreach {title uri} $item break
        if {$title eq "-"} {
            .m.imgs add separator
            continue
        }
        if {! [string match "http:*" [string tolower $uri]]} {
            if {! [file exists $uri]} continue
            .m.imgs add command -label $title -command [list DoOpen $uri]
        } else {
            .m.imgs add command -label $title -command [list DoOpen $uri] \
                -accelerator "(web)"
        }
    }
 }


 proc DIE {emsg} {
    tk_messageBox -message $emsg -icon error -title $::S(title)
    exit
 }
 proc WARN {emsg} {
    tk_messageBox -message $emsg -icon error -title $::S(title)
 }
 proc CountColors {img} {
    set iw [image width  $img]
    set ih [image height $img]
    array unset C

    for {set y 0} {$y < $ih} {incr y} {
        set y2 [expr {$y + 1}]
        set data [$img data -from 0 $y $iw $y2]
        foreach datum [lindex $data 0] {
            set C($datum) 1
        }
    }
    set cnt [llength [array names C]]
    return $cnt
 }
 proc MakeFrame {w who title {btn 0}} {
    destroy $w
    frame $w
    label $w.title -text $title -font {Times 24 bold}
    label $w.image -image ::img::$who -relief ridge
    label $w.clrs -textvariable ::S(colors,$who) -font {Times 12 bold}
    set ::S(colors,$who) "? colors"
    if {$btn == 1} {
        button $w.btn -text Go -command [list Demo $who] -state disabled
        lappend ::S(btns) $w.btn
    } elseif {$btn == 2} {
        scale $w.shades -variable S(numShades) -from 2 -to 256 \
            -label "\# Shades" -orient h -relief ridge
    }
    foreach child [winfo child $w] { grid $child -sticky n }
    grid rowconfigure $w 100 -weight 1
    return $w
 }
 proc Demo {who} {
    global S

    ButtonState disabled
    if {$who eq "avg"} {
        ::img::avg blank
        ::img::avg put \#00FFFF -to 0 0 $::iw $::ih
        set S(colors,avg) "$S(numShades) colors"
        AverageDither $S(numShades) ::img::src ::img::avg
    } elseif {$who eq "rnd"} {
        ::img::rnd blank
        ::img::rnd put \#FFFF00 -to 0 0 $::iw $::ih
        set S(colors,rnd) "2 colors"
        RandomDither $S(numShades) ::img::src ::img::rnd
    } elseif {$who eq "ord"} {
        ::img::ord blank
        ::img::ord put \#FF8080 -to 0 0 $::iw $::ih
        set S(colors,ord) "2 colors"
        OrderedDither $S(numShades) ::img::src ::img::ord
    } else {
        ::img::fs blank
        ::img::fs put \#FF00FF -to 0 0 $::iw $::ih
        set S(colors,fs) "$S(numShades) colors"
        Floyd-Steinberg $S(numShades) ::img::src ::img::fs
    }
    ButtonState normal
 }
 proc ButtonState {how} {
    foreach w $::S(btns) { $w config -state $how}
 }

 proc DoOpen {{fname ""}} {
    if {$fname eq ""} {
        set fname [tk_getOpenFile]
    }
    if {$fname eq ""} return
    set n [OpenURI ::img::org $fname]
    if {! $n} {
        ButtonState disabled
    } else {
        MakeImages
        DoDisplay
        Demo avg
        Demo fs
        Demo rnd
        Demo ord
    }
 }

 proc OpenURI {img fname} {
    catch {image delete $img}
    image create photo $img
    ButtonState disabled

    if {[file exists $fname]} {
        $img config -file $fname
        ButtonState normal
        return 1
    }
    if {! [string match "http:*" [string tolower $fname]]} {
        WARN "Can't find '$fname'"
        return 0
    }
    destroy .http
    toplevel .http
    wm transient .http .
    wm title .http "Download"
    label .http.t -text "Web Download Progress" -font {Times 12 bold}
    label .http.l -textvariable S(msg)

    pack .http.t .http.l -side top -expand 1
    set wh [winfo reqheight .http]     ; set ww [winfo reqwidth .http]
    set sw [winfo width .]             ; set sh [winfo height .]
    set sy [winfo y .]                 ; set sx [winfo x .]
    set x [expr {$sx + ($sw - $ww)/2}] ; set y [expr {$sy + ($sh - $wh)/2}]
    if {$x < 0} { set x 0 }            ; if {$y < 0} {set y 0}
    wm geometry .http +$x+$y

    set token [::http::geturl $fname -progress HttpProgress]
    ::http::wait $token
    destroy .http
    if {[::http::ncode $token] != 200} {
        ::http::cleanup $token
        WARN "Error downloading url"
        return 0
    }

    $img config -data [::http::data $token]
    ButtonState normal
    ::http::cleanup $token
    return 1
 }
 proc HttpProgress {token total current} {
    set ::S(msg) "[comma $current]/[comma $total]"
    set data [::http::data $token]
    catch {::img::src config -data $data}
    update
 }
 proc comma { num } {
    while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {}
    return $num
 }

 proc MakeImages {} {
    global iw ih

    set iw [image width ::img::org]
    set ih [image height ::img::org]

    if {$iw == 0} {set iw 300; set ih 420 }
    image create photo ::img::src -width $iw -height $ih
    ::img::src put [::img::org data -grayscale]

    image create photo ::img::avg -width $iw -height $ih
    ::img::avg put cyan -to 0 0 $iw $ih
    image create photo ::img::fs -width $iw -height $ih
    ::img::fs put magenta -to 0 0 $iw $ih
    image create photo ::img::rnd -width $iw -height $ih
    ::img::rnd put yellow -to 0 0 $iw $ih
    image create photo ::img::ord -width $iw -height $ih
    ::img::ord put \#FF8080 -to 0 0 $iw $ih
 }
 proc Help {} {
    catch {destroy .help}
    toplevel .help
    wm transient .help .
    wm title .help "Dither Help"
    if {[regexp {(\+[0-9]+)(\+[0-9]+)$} [wm geom .] => wx wy]} {
        wm geom .help "+[expr {$wx+35}]+[expr {$wy+35}]"
    }
    set w .help.t
    scrollbar .help.sb -orient vertical -command [list $w yview]
    text $w -wrap word -width 70 -height 30 -pady 10 -yscroll {.help.sb set}
    button .help.quit -text Dismiss -command {catch {destroy .help}}
    pack .help.quit -side bottom
    pack $w -side left -fill both -expand 1
    pack .help.sb -side right -fill y

    set margin [font measure [$w cget -font] " o "]
    set margin2 [font measure [$w cget -font] " o - "]
    $w tag config header -justify center -font bold -foreground red
    $w tag config header2  -justify center -font bold
    $w tag config bullet -lmargin2 $margin -fon "[$w cget -font] bold"
    $w tag config n -lmargin1 $margin2 -lmargin2 $margin2

    $w insert end "Dither" header "\nby Keith Vetter\nJuly 2006\n\n" header2

    $w insert end "Here are some common dithering algorithms for gray scaled "
    $w insert end "images. They can easily be extended to color images. "
    $w insert end "Also of interest--but not done here--is to first "
    $w insert end "optimize the palette before dithering. See "
    $w insert end "https://wiki.tcl-lang.org/11234 for such an algorithm\n\n"

    $w insert end " o Average dither\n" bullet
    $w insert end "One of the simplest dithering techniques, based on " n
    $w insert end "selecting an average tone and choosing pixel colors " n
    $w insert end "based on how close they are to the average.\n\n" n

    $w insert end " o Floyd-Steinberg dither\n" bullet
    $w insert end "A dithering algorithm by Robert Floyd and " n
    $w insert end "Louis Steinberg (1976). The algorithm achieves " n
    $w insert end "dithering by diffusing the quantization error of " n
    $w insert end "a pixel to its neighboring pixels.\n\n" n

    $w insert end " o Random dithering\n" bullet
    $w insert end "For each value in the image, simply generate a random " n
    $w insert end "number 1..256; if it is greater than the image value " n
    $w insert end "at that point, plot the point white, otherwise plot " n
    $w insert end "it black.  This generates a picture with a lot of " n
    $w insert end "\x22white noise\x22, which looks like TV picture " n
    $w insert end "\snow. This algorithm can be used to remove " n
    $w insert end "\"artifacts\" which are phenomena produced by digital " n
    $w insert end "signal processing.\n\n" n

    $w insert end " o Ordered dither\n" bullet
    $w insert end "A fast algorithm which produces a cross-hatch dithering " n
    $w insert end "pattern similar to the halftones used by print newspapers." n
    $w insert end " It tiles a 4x4 threshold matrix on top of the image. If " n
    $w insert end "the value of a pixel (scaled to 0-16 range) is less than " n
    $w insert end "the number in the corresponding cell in the matrix, draw " n
    $w insert end "it black, otherwise draw it white.\n\n" n
    $w config -state disabled
 }

 image create photo ::img::org
 MakeImages
 DoDisplay

 set done 0
 if {$argc > 0} {
    DoOpen [lindex $argv 0]
    set done 1
    return
 }
 foreach img [list lena.gif $teapotImg] {
    if {[file exists $img]} {
        DoOpen $img
        set done 1
        return
    }
 }
 if {! $done} {
    set msg "Cannot find standard image for demo.\n\n"
    append msg "Download from the net?"
    set val [tk_messageBox -message $msg -icon info -title $S(title) -type yesno]
    if {$val eq "yes"} {
        DoOpen $lenaURL
    }
 }

 return