[comp.graphics] Floyd-Steinberg Errors: What do I do with them?

ewhac@well.UUCP (Leo 'Bols Ewhac' Schwab) (07/12/88)

[ Maybe I should write an automatic line-eater line generator... ]

	Hi there.  After some helpful suggestions from this group, I've
started playing around with the Floyd-Steinberg image quantization algorithm.
You know, the one where you distribute the quantization error to neighboring
pixels.

	However, I've run into a weird snag.  Occasionally, depending on the
Phase of The Moon, it translates black pixels into grey ones.  Needless to
say, this is The Wrong Thing.  After considering a number of possible
problems, it struck me that perhaps I'm not dealing with overflow and
underflow correctly.

	Consider the following scenario:  Suppose we have two monochromatic
pixels side by side with the following intensities (0 == black, 1 == white):

	+-----+-----+
	| .51 | .02 |		Threshold is .5
	+-----+-----+

	So Floyd and Steinberg come along, see the .51 pixel, and quantize
it to 1.0.  By the rules of the algorithm, the pixel immediately to the
right gets 3/8 of the quantization error.  This leaves us with:

	+-----+-----+
	| 1.0 |-.16 |		((.51 - 1.0) * 3 / 8) + .02  ==  -.16
	+-----+-----+

	Now clearly, -.16 is out of range for the value of a pixel; it's too
black.

	So my question is this:  What do I do with this pixel when it comes
time to process it?  Do I pretend like there's no problem, and treat it like
all the other pixels (doesn't seem right, since this negative pixel will
suck the brightness out of the neighboring pixel when I process it)?  Do I
I truncate its value to the valid range (also seems wrong, since I'm tossing
part of the error out the window)?  If I do truncate it, when should I do
it: when I encounter the errant pixel and process it; or while I'm
distributing the error to it, thus "preventing the problem in the first
place"?

	Thanks in advance.

_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
Leo L. Schwab -- The Guy in The Cape	INET: well!ewhac@ucbvax.Berkeley.EDU
 \_ -_		Recumbent Bikes:	UUCP: pacbell > !{well,unicom}!ewhac
O----^o	      The Only Way To Fly.	      hplabs / (pronounced "AE-wack")
"Hmm, you're right.  Air is made up of suspended meat loaf."  -- Josh Siegel

sclafani@jumbo.dec.com (Michael Sclafani) (07/13/88)

In article <6506@well.UUCP>, ewhac@well.UUCP (Leo 'Bols Ewhac' Schwab) writes:
> 	However, I've run into a weird snag.  Occasionally, depending on the
> Phase of The Moon, it translates black pixels into grey ones.

This sounds like a common implementation problem: if you don't handle the
error terms properly, your error terms will accumulate and grow, whiting
out the image.  Make sure that the sum of the computed error terms equals
the actual error.  It's easy to get this wrong if you use bit-shifts for
division or aren't careful about rounding.

> After considering a number of possible
> problems, it struck me that perhaps I'm not dealing with overflow and
> underflow correctly.

[ example of negative pixel value deleted ]

> 	Now clearly, -.16 is out of range for the value of a pixel; it's too
> black.
> 
> 	So my question is this:  What do I do with this pixel when it comes
> time to process it?  Do I pretend like there's no problem, and treat it like
> all the other pixels (doesn't seem right, since this negative pixel will
> suck the brightness out of the neighboring pixel when I process it)?

But is _is_ right.  You've taken a pixel which is "gray", and set it to
"white".  To compensate, other pixels must be made darker.  Since the
adjacent pixel would already have been "black", the negative error term
will propagate through the image.  Consider a one-dimensional image (all
of the error propagates to the right):

 	+-----+-----+-----+
 	| .51 | .02 | .51 |		(.51 - 1.0) + .02  ==  -.47
 	+-----+-----+-----+

 	+-----+-----+-----+
 	| 1.0 | 0.0 | .51 |		(-.47 - 0.0) + .51  ==  .04
 	+-----+-----+-----+

 	+-----+-----+-----+
 	| 1.0 | 0.0 | 0.0 | +.04
 	+-----+-----+-----+
 
The average intensity of the region has been maintained _because_ of the
propagation of negative values.  The range of legal pixel values doubles
from [ 0.0 , 1.0 ] to [ -0.5 , 1.5 ].

In the example you gave, you used 3/8 to determine the horizontal error
term.  I believe that is different from the technique that Floyd and
Steinberg actually used, with the error split in four directions, not just
three:
        +-----+-----+-----+
        |     |  x  | 7/16|
        +-----+-----+-----+
        | 3/16| 5/16| 1/16|
        +-----+-----+-----+
         
This method introduces fewer artifacts than the (3/8 3/8 2/8) technique.
I think Steinberg posted on this newsgroup, stating that alternating
directions on scan lines (left to right on odd, right to left on even)
will produce even better results.  I've seen an implementation which does
this, and it's true.

Michael Sclafani     \\\  Digital Equipment Corporation
sclafani@src.dec.com \\\  Systems Research Center, Palo Alto, CA

aden@versatc.UUCP (Michael Aden) (07/15/88)

In article <13172@jumbo.dec.com> sclafani@jumbo.dec.com (Michael Sclafani) writes:
>In article <6506@well.UUCP>, ewhac@well.UUCP (Leo 'Bols Ewhac' Schwab) writes:
... lots of error conversation here...

>I think Steinberg posted on this newsgroup, stating that alternating
>directions on scan lines (left to right on odd, right to left on even)
>will produce even better results.  I've seen an implementation which does
>this, and it's true.

There is a relatively new book on digital halftoning which discusses Floyd-Steinberg
error terms, as well as a half-dozen others which cover this topic fairly effectively.
The book is 
_Digital Halftoning_, by Robert Ulichney, (c)1987 MIT Press.

Serpentine treatment of raster gets pretty much a full chapter near the end of the
book (the book goes generally from worst-case to best-case rasterization, hence
serpentining's later position).

The problem with Floyd-Steinberg (and all the others) when used on scanlines
is a tendency towards "worm tracks", a propagation of error in a coherent direction.
While serpentine helps this, even better would be error propagation along
a non-trivial path, which leads us back to the Peano curve discussion of a few months
back. I never saw a discussion of how to do those fast. Anybody have any hints
they'd like to share? (I'll keep my eyes open this time -> 8-) )

BTW, I'd like to do this on an 8000x8000 image, so efficiency is kind of important.


Michael Aden
Versatec, Inc.
{pyramid,vsi1,uunet}!versatc!aden

dbeck@unix.SRI.COM (Douglas K. Beck) (07/17/88)

There is a later paper by Ulnichy that describes methods for modifying
the FS dither, using a random perturbation of the threshold for each
pixel, and distributing the error on a randomized basis between the
next adjacent pixel and the pixel directly beneath the object pixel.
It gives smoother looking images, albeit noisy than any other implementation
of the FS scheme that I have tried.
...doug beck   dbeck@unix.sri.com

jbm@eos.UUCP (Jeffrey Mulligan) (07/19/88)

From article <252@versatc.UUCP>, by aden@versatc.UUCP (Michael Aden):

[stuff deleted] ...

> The problem with Floyd-Steinberg (and all the others) when used on scanlines
> is a tendency towards "worm tracks", a propagation of error in a coherent direction.
> While serpentine helps this, even better would be error propagation along
> a non-trivial path, which leads us back to the Peano curve discussion of a few months
> back. I never saw a discussion of how to do those fast.


You might be interested in:

Koenderink, J.J., and Van Doorn, A.J.,
"New type of raster scan preserves the topology of the image."
Proc. IEEE, v.67, no. 19, pp. 1465-1466, Oct. 1979.


-- 

	Jeff Mulligan (jbm@aurora.arc.nasa.gov)
	NASA/Ames Research Ctr., Mail Stop 239-3, Moffet Field CA, 94035
	(415) 694-6290

ma_jns@ux63.bath.ac.uk (Spackman) (07/19/88)

   2N     11776: 19 Jul 88 John Spackman   
(Message # 2: 11776 bytes, New)
Via:  uk.ac.bath.maths; 19 Jul 88 10:41 BST
Received: from chroma by mordell.maths.bath.AC.UK id aa29121;
          19 Jul 88 10:35 BST
Date:     Tue, 19 Jul 88 10:33:28 BST
From:     John Spackman <jns@uk.ac.bath.maths>
To:       ma_jns@uk.ac.bath.ux63

               The following shell archive contains various C code for
          mappings  between  Cartesian  and  Peano  co-ordinates.  The
          Peano co-ordinate of a point in a 2^n  side  square  is  the
          position  along the connected, filling Peano curve ( Hilbert
          here ).  The mappings are achieved with  simple  bit  shifts
          for speed, iterating 'n' times for a '2^n' side square.

               I have used this curve to order error propagation (c.g.
          Floyd  Steinberg), passing the whole error incurred by quan-
          tisation at one pixel to the Peano-next pixel.  Other graph-
          ical applications include run-length images ALONG THE CURVE,
          which is comparable to quad encoding.

               The code may be adapted to generate curves in  n-space,
          e.g. 3-space for look up table algorithms.

    John Spackman, Maths Sciences
    University of Bath, Claverton  Down,  Bath,  Avon,  BA2 7AY, England.

#------------- cut here -------------- cut here -----------
#! /bin/sh
# This is a shell archive.  Remove anything before this line, then unpack
# it by saving it into a file and typing "sh file".  To overwrite existing
# files, type "sh file -c".  You can also feed this as standard input via
# unshar, or by typing "sh <file", e.g..  If this archive is complete, you
# will see the following message at the end:
#		"End of archive 1 (of 1)."
# Contents:  peano_routines.h peano_routines.c example.c makefile
# 
# An example showing how to use the mappings is given in "example.c", 
# which you should make with the makfile. For example, try :-
# Level (>0) ? 1                                  'Square of 2^level side
# Direction : Across in [0,1] & From in [0,3] ?   'Direction & Origin of curve
#                                                 'Output ..
# 0 th peano point AT [ X = 0, Y = 0 ]
# 	[ X = 0, Y = 0 ] AT 0 th peano point
# 1 th peano point AT [ X = 1, Y = 0 ]
# 	[ X = 1, Y = 0 ] AT 1 th peano point
# 2 th peano point AT [ X = 1, Y = 1 ]
# 	[ X = 1, Y = 1 ] AT 2 th peano point
# 3 th peano point AT [ X = 0, Y = 1 ]
# ... etc.
if test -f 'peano_routines.h' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'peano_routines.h'\"
else
echo shar: Extracting \"'peano_routines.h'\"
sed "s/^X//" >'peano_routines.h' <<'END_OF_FILE'
Xstruct traverse
X{ 
X  int accross, from;
X};
X
Xstruct record
X{ 
X  int moreton, peano;
X  struct traverse next;
X};
X
Xstruct path
X{ 
X  struct record quad[4];
X};
X
Xstruct curve
X{ 
X  struct path dimension[2];
X};
X
Xtypedef struct traverse TRAVERSE;
Xtypedef struct record RECORD;
Xtypedef struct path PATH;
Xtypedef struct curve CURVE;
END_OF_FILE
fi
if test -f 'peano_routines.c' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'peano_routines.c'\"
else
echo shar: Extracting \"'peano_routines.c'\"
sed "s/^X//" >'peano_routines.c' <<'END_OF_FILE'
X#include "peano_routines.h"
X
Xchar copy_right[] = "\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X        |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   | \n\
X        |   +---+   |   |   +---+   |   |   +---+   |   |   +---+   | \n\
X        |           |   |           |   |           |   |           | \n\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X            |   |           |   |           |   |           |   |     \n\
X        +---+   +-----------+   +---+   +---+   +-----------+   +---+ \n\
X        |                           |   |                           | \n\
X        |   +-------+   +-------+   |   |   +-------+   +-------+   | \n\
X        |   |       |   |       |   |   |   |       |   |       |   | \n\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X                |           |                   |           |         \n\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X        |   |       |   |       |   |   |   |       |   |       |   | \n\
X        |   +-------+   +-------+   +---+   +-------+   +-------+   | \n\
X        |                                                           | \n\
X        +---+   +-------+   +-------+   +-------+   +-------+   +---+ \n\
X            |   |       |   |       |   |       |   |       |   |     \n\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X        |           |           |           |           |           | \n\
X        |   +---+   |   +---+   +---+   +---+   +---+   |   +---+   | \n\
X        |   |   |   |   |   |       |   |       |   |   |   |   |   | \n\
X        +---+   +---+   |   +-------+   +-------+   |   +---+   +---+ \n\
X                        |                           |                 \n\
X        +---+   +---+   |   +-------+   +-------+   |   +---+   +---+ \n\
X        |   |   |   |   |   |       |   |       |   |   |   |   |   | \n\
X        |   +---+   |   +---+   +---+   +---+   +---+   |   +---+   | \n\
X        |           |           |           |           |           | \n\
X        +---+   +---+   +---+   +---+   +---+   +---+   +---+   +---+ \n\
X            |   |       |   |       |   |       |   |       |   |     \n\
X        >>--+   +-------+   +-------+   +-------+   +-------+   +-->> \n\
X                                                                      \n\
X    ######                                         @1988 John Spackman,   \n\
X     ##  ##                                        The University of Bath,\n\
X     ##  ##   ####    ####    #####    ####        Claverton Down,        \n\
X     #####   ##  ##      ##   ##  ##  ##  ##       Bath,                  \n\
X     ##      ######   #####   ##  ##  ##  ##       Avon,                  \n\
X     ##      ##      ##  ##   ##  ##  ##  ##       BA2 7AY,               \n\
X    ####      ####    ### ##  ##  ##   ####        England.               \n\
X                                                                          \n\
X      ####                                                                \n\
X     ##  ##                                                               \n\
X    ##       ##  ##   ## ###   ##  ##   ####   #####                      \n\
X    ##       ##  ##    ### ##  ##  ##  ##  ## ##                          \n\
X    ##       ##  ##    ##  ##  ##  ##  ######  ####                       \n\
X     ##  ##  ##  ##    ##       ####   ##         ##                      \n\
X      ####    ### ##  ####       ##     ####  #####                       \n\
X\n";
X
Xstatic CURVE pattern;
Xstatic int level, power;
X
Xinit(degree)
Xint degree;
X{   
X  PATH *x = &(pattern.dimension[0]);
X  PATH *y = &(pattern.dimension[1]);
X
X  x->quad[0].moreton = 0;     
X  x->quad[0].peano = 0;
X  x->quad[0].next.accross = 1;
X  x->quad[0].next.from = 0;
X  x->quad[1].moreton = 2;     
X  x->quad[1].peano = 3;
X  x->quad[1].next.accross = 0;
X  x->quad[1].next.from = 0;
X  x->quad[2].moreton = 3;     
X  x->quad[2].peano = 1;
X  x->quad[2].next.accross = 0;
X  x->quad[2].next.from = 0;
X  x->quad[3].moreton = 1;     
X  x->quad[3].peano = 2;
X  x->quad[3].next.accross = 1;
X  x->quad[3].next.from = 3;
X
X  y->quad[0].moreton = 0;     
X  y->quad[0].peano = 0;
X  y->quad[0].next.accross = 0;
X  y->quad[0].next.from = 0;
X  y->quad[1].moreton = 1;     
X  y->quad[1].peano = 1;
X  y->quad[1].next.accross = 1;
X  y->quad[1].next.from = 0;
X  y->quad[2].moreton = 3;     
X  y->quad[2].peano = 3;
X  y->quad[2].next.accross = 1;
X  y->quad[2].next.from = 0;
X  y->quad[3].moreton = 2;     
X  y->quad[3].peano = 2;
X  y->quad[3].next.accross = 0;
X  y->quad[3].next.from = 3;
X
X  level = degree; 
X  power = degree*2;
X
X}
X
Xint from_peano(peano,direction)
Xint peano;
XTRAVERSE direction; /* Position along Peano curve */
X{   
X  int bits = power;
X  int from = direction.from;
X  int peano_quad;
X  int result = 0;
X  RECORD use;
X
X  while (bits)
X  {   
X    bits -= 2;
X    peano_quad = (peano>>bits)&3;
X    use = pattern.dimension[direction.accross].quad[peano_quad];
X    direction = use.next;
X    result |= ((from^use.moreton)<<bits);
X    from ^= direction.from;
X  }
X
X  return result;
X}
X
Xint to_peano(moreton,direction)
Xint moreton;
XTRAVERSE direction; /* Position along Peano curve */
X{   
X  int bits = power;
X  int from = direction.from;
X  int moreton_quad, peano_quad;
X  int result = 0;
X  RECORD use;
X
X  while (bits)
X  {   
X    bits -= 2;
X    moreton_quad = (moreton>>bits)&3;
X    peano_quad =
X        pattern.dimension[direction.accross].quad[from^moreton_quad].peano;
X
X    use = pattern.dimension[direction.accross].quad[peano_quad];
X    direction = use.next;
X    result |= (peano_quad<<bits);
X    from ^= direction.from;
X  }
X
X  return result;
X}
X
Xcoord(moreton,x,y)
Xint moreton;
Xint *x, *y;
X{   
X  int bits = 1;
X  *x = 0; 
X  *y =0;
X
X  while (moreton)
X  {   
X    if (moreton&1) *x |= bits;
X    moreton >>= 1;
X    if (moreton&1) *y |= bits;
X    moreton >>= 1;
X    bits <<= 1;
X  }
X  return;
X}
X
Xint combine(x,y)
Xint x, y;
X{   
X  int result =0;
X  int bits = 1;
X
X  while (x || y)
X  {   
X    if (x&1) result |= bits;
X    x >>=1; 
X    bits <<=1;
X    if (y&1) result |= bits;
X    y >>=1; 
X    bits <<=1;
X  }
X  return result;
X}
END_OF_FILE
fi
if test -f 'example.c' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'example.c'\"
else
echo shar: Extracting \"'example.c'\"
sed "s/^X//" >'example.c' <<'END_OF_FILE'
X#include <stdio.h>
X#include "peano_routines.h"
X
Xextern char copy_right[];
X
Xmain()
X{   
X  TRAVERSE direct;
X  int moreton, peano, x, y;
X  int i, top, level;
X  int entered;
X  
X  printf(copy_right);
X
X  do
X  { do
X    { printf("Level (>0) ? ");
X      entered = scanf("%d", &level);
X      while (getchar()!='\n') continue;
X    }   
X    while (level <=0 || entered<1);
X
X    init(level);
X    top = 1<<(level*2);
X
X    do
X    { printf("Direction : Across in [0,1] & From in [0,3] ? ");
X      entered = scanf("%d%d", &(direct.accross), &(direct.from) );
X      while (getchar()!='\n') continue;
X    }   
X    while (direct.accross  < 0 || direct.accross  > 1 ||
X        direct.from < 0 || direct.from > 3 ||
X        entered<2);
X
X    for (i=0; i<top; i++)
X    { moreton =  from_peano(i,direct);
X      coord(moreton,&x,&y);
X      printf("%d th peano point AT [ X = %d, Y = %d ]\n",i,x,y);
X      moreton = combine(x,y);
X      peano = to_peano(moreton,direct);
X      printf("\t[ X = %d, Y = %d ] AT %d th peano point\n",x,y,peano);
X    }
X
X  } 
X  while (level);
X
X}
END_OF_FILE
fi
if test -f 'makefile' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'makefile'\"
else
echo shar: Extracting \"'makefile'\"
sed "s/^X//" >'makefile' <<'END_OF_FILE'
X# Makefile for peano routines
X
XSDIR= .
XODIR= .
XRDIR= .
XBACKUP_DIR= /tmp
X
X# C Source
X
XSRCS= $(SDIR)/example.c $(SDIR)/peano_routines.c
X
X# Object Code
X
XOBJS= $(ODIR)/example.o $(ODIR)/peano_routines.o
X
X# Runs
X
XRUNS= $(RDIR)/example 
X
X# Executable files to be created
X
Xall: example 
X
X# Compile commands
X
Xexample: example.o peano_routines.o
X	cc -g -o example example.o peano_routines.o
Xexample.o:
X	cc -g -c $(SDIR)/example.c
X
Xpeano_routines.o: peano_routines.h
X	cc -g -c $(SDIR)/peano_routines.c
X
X# Lint command
X
Xlint: $(SRCS)
X	lint $(SRCS)
X
X# Remove unnecessary files
X
Xclean:
X	rm -f $(ODIR)/core $(ODIR)/a.out $(ODIR)/*.o $(SDIR)/*~
END_OF_FILE
fi

lou@bearcat.rutgers.edu (Lou Steinberg) (07/22/88)

In article <252@versatc.UUCP> aden@versatc.UUCP (Michael Aden) writes:

> The problem with Floyd-Steinberg (and all the others) when used on scanlines
> is a tendency towards "worm tracks", a propagation of error in a coherent direction.

I don't believe the problem is "propagation of error in a coherent
direction" so much as *lack* of propagation in a coherent direction.
To state it more clearly (but less poetically :-), the problem is the
existence of two pixels placed such that no error propagates between
them, even indirectly.  For instance, in raster scan Floyd-Steinberg,
using our original 4-neighbor method, no error propagates from a pixel
to the pixel one line below and two pixels back, so that when the first
pixel turns on there is no way for this to inhibit the other pixel
from firing.  In a region of constant low brightness, however, the
errors tend to build up more or less uniformly, so if the first pixel
is on, chances are good the second pixel, not being inhibited, will be
on as well, leading to worm tracks at this "knight's move" (one down
and two back) orientation.

With a serpentine scan, there is no such pattern of points that do not
have at least an indirect error propagation path, and worm tracks are
not a problem.  There are other artifacts, such as apparent lines
where a stable pattern (e.g. a checkerboard) suddenly breaks down or
where two patterns meet out of phase (e.g. two checkerboards, one
shifted a row down).

(As a point of credit, the serpentine scan was first suggested by John
Roach, one of our grad students, but as far as I know was rediscovered
independently by Ulichney, and I think Ulichney was the first to
publish it.)

> While serpentine helps this, even better would be error propagation along
> a non-trivial path, which leads us back to the Peano curve discussion of a few months
> back. I never saw a discussion of how to do those fast. Anybody have any hints
> they'd like to share? 

The idea of propagating error along a Peano curve scan path was
suggested by Witten and Neal of the University of Calgary in the May
82 Computer Graphics and Applications ("Using Peano Curves for Bilevel
Display of Continuous Tone Images").  They only propagate the error
forward along the scan, rather than also off to the sides as in error
diffusion, and the result is (to my biased eye) not as good.  (They
had apparently never heard of our method when they published the
paper.)  I've never seen anyone try a peano curve with error off to
the sides.  It sounds a bit messy, and I'm dubious that the results
would be much better than serpentine scan, but I'd be interested to
hear if anyone tries it.  Witten and Neal do not actually cover
the whole image with one peano curve - they cover a box (e.g. 64x64),
and replicate that over the image.  Probably you could pre-compute the
peano path in the form of a list of offsets (+/- 1 or unchanged for x
and y) that take you from one point to the next within the box, so
that the extra cost of the scan would not add that much to the whole
cost per pixel - and would perhaps be even faster than standard
Floyd/Steinberg, since you would not have to compute the fractional
errors.  On the other hand, if the individual bits of the
bi-level version of the picture are packed up into bytes, accessing
them in peano order may be pretty expensive.
-- 
					Lou Steinberg

uucp:   {pretty much any major site}!rutgers!aramis.rutgers.edu!lou 
arpa:   lou@aramis.rutgers.edu