Binary image compression challenge - RS's entry

Summary

Richard Suchenwirth 2004-09-06: Here's my take for the Binary image compression challenge, as proposed by KBK.

Description

It assumes that the colors of the binary image are #000000 (black) and #FFFFFF (white), and

  • turns the pixels into a bitstream,
  • converts that to a sequence of positive runlengths
  • encodes those compactly with Elias coding (gamma)

Results (compressed size in bytes, test runtime on my 200MHz box):

  • courier12.gif - 585 in 5.1 sec
  • times12i.gif - 586 in 4.9 sec
  • castle.gif - 9993 in 345 sec :(
  • ouster.gif - 1613 in 19.6 sec
  • cat.gif - 18633 in 45 minutes :(

Just for the record, I also tested Elias' delta coding, but it fared worse:

courier12: 615 times12i: 660 castle: 11173 ouster: 1774

This is intuitively explained by the fact that short runs (<16) are predominant in the sample images.

proc Elias'encode img {
    set h [image height $img]
    set w [image width  $img]
    set bits ""
    foreach row [$img data] {
        foreach pixel $row {
             append bits [string equal $pixel #000000]
        }
     }
     set runs [map strlen [split'runs $bits]]
     binary format ssb* $h $w [Elias'gammas $runs]
}
proc Elias'decode data {
    binary scan $data ssb* h w ebits
    set data {}
    set bit 1
    set bits ""
    foreach run [Elias'decode'gammas $ebits] {
        append bits [string repeat $bit $run]
        while {[string length $bits]>=$w} {
            lappend data [bits2cols [string range $bits 0 [expr {$w-1}]]]
            set bits [string range $bits $w end]
        }
        set bit [expr {!$bit}]
    }
    set img [image create photo -width $w -height $h]
    $img put $data -to 0 0
    set img
}
proc Elias'test img {
    set data [Elias'encode $img]
    set img2 [Elias'decode $data]
    if {[$img data] ne [$img2 data]} {error "result not equal"}
    image delete $img2
    string length $data
}
proc bits2cols bits {
    set res {}
    foreach bit [split $bits ""] {
        lappend res [expr {$bit? "#FFFFFF" : "#000000"}]
    }
    set res
}
proc map {fn list} {
    set res {}
    foreach e $list {lappend res [$fn $e]}
    set res
}
interp alias {} strlen {} string length ;# to make it a one-worder

proc split'runs bits {
    string map {01 "0 1"} [string map {10 "1 0"} $bits]
}
#-- See [Elias coding] for explanations on these functions
proc Elias'gamma int {
    set bits [int2bits $int]
    return [string repeat 0 [expr {[string length $bits]-1}]]$bits
}
proc Elias'gammas ints {
    set res ""
    foreach int $ints {append res [Elias'gamma $int]}
    set res
}
proc Elias'decode'gammas bits {
    set res {}
    while {$bits ne ""} {
        regexp ^(0*) $bits -> zeroes
        set length [expr {[string length $zeroes]*2}]
        lappend res [bits2int [string range $bits 0 $length]]
        set bits [string range $bits [incr length] end]
    }
    set res
}
proc bits2int bits {
    set res 0
    foreach bit [split $bits ""] {set res [expr {$res*2+$bit}]}
    set res
}
proc int2bits int {
    set res ""
    while {$int>0} {
       set res [expr {$int%2}]$res
       set int [expr {$int/2}]
    }
    set res
}