Dithern: Graustufenbilder für S/W-Drucker aufbereiten

Wer schon einmal versucht hat Graustufenbilder auf einem Drucker auszugeben, wird von den Ergebnissen meist enttäuscht worden sein. Die Bilder werden beim Ausdruck meist zu dunkel und der Kontrast ist kaum vorhanden. Viele Programme bieten nun Verfahren an, mit denen ein Bild für den Ausdruck vorbereitet wird. Der Weg zu einem brauchbaren Ergebnis ist gepflastert von unzähligen Probeausdrucken und Justierungen.

Das hier vorgestellte Verfahren simuliert ganz einfach den Drucker und bestimmt so die Graustufenrepresentation. Das Ergebnis sind sofort brauchbare Bilder, deren Qualität sich mit anderen Verfahren noch verbessern läßt.

Von Punkten und Helligkeit

Erst einmal muß man sich überlegen, wie sich denn nun unterschiedliche Helligkeiten aus den zwei möglichen des Druckers (nämlich Weiß und Schwarz) zusammensetzen lassen.

Man macht sich hierbei die Fähigkeit des Auges zunutze, eng beieinander liegende Punkte als Helligkeitsinformation aufzufassen. So erscheint uns z.B. der Desktop-Hintergrund als grau, obwohl er eigentlich nur aus schwarzen und weißen Punkten besteht. Wenn man jetzt eine bestimmte Fläche von 8x8 Pixeln Größe betrachtet, läßt sich ein Muster feststellen:

Auf einen weißen Punkt folgt jeweils ein schwarzer. Dieses Muster nennt man auch Raster.

Da schwarze und weiße Punkte in unserer Fläche mit der gleichen Häufigkeit auftreten, ist hier eine Graustufe von 50% dargestellt. Sind alle Punkte weiß, wären es 0%, sind alle schwarz, 100%. Wie wir sehen, können also unterschiedliche Helligkeitseindrücke mit der Häufigkeit von schwarzen und weißen Punkten einer Fläche dargestellt werden. Wenn wir bei einer Fläche von 8x8 Punkten bleiben, können also maximal 64 Punkte schwarz sein. Bedenkt man jetzt noch, daß auch alle Punkte weiß bleiben können, erhalten wir 65 mögliche Graustufen mit einem 8x8-Raster.

Darstellung durch Raster

Mit einer Fläche von 8x8 schwarzen und weißen Punkten lassen sich also 65 Graustufen darstellen, indem jeder Punkt des Graustufenbildes durch eben eines dieser Raster ersetzt wird. Wenn jetzt allerdings jeder Punkt des Ursprungsbildes zu einer Rasterfläche wird, vergrößert sich dieser Punkt natürlich entsprechend. Die Auflösung wird schlechter. Ein Gerät, daß 300 dpi, also 300 Punkte pro Zoll darstellen kann, hat dann bei einem 8x8-Raster eine Auflösung von 37,5 dpi.

Damit dies nicht so ins Gewicht fällt, erdachte man ein Verfahren, das sich „Multischwellwertverfahren“ nennt. Hier wird z.B. eine 2x2-Matrix (Dither-matrix) von Schwellwerten vorgegeben.

**2x2-Dithermatrix **

  1 2
1 51 153
2 204 102

Zur Ausgabe eines Bildpunktes mit den Koordinaten (x,y) wird dann mit rx = x modulo 2 und ry = y modulo 2 die entsprechende Position (rx,ry) in der Dithermatrix berechnet. Wenn in dem Graustufenbild die geringste Schwärzung einen Wert von 0 und die höchste Schwärzung einen Wert von 255 hat, so wird der Punkt dann schwarz, wenn der Grauwert größer ist als der jeweilige Schwellenwert.

Das entstehende Muster ist zwar das gleiche, das auch durch ein Raster entsteht, allerdings wird so jedem Punkt ein bestimmtes Teilraster zugeordnet. Hierdurch wird eine Vergrößerung des Bildes vermieden. Das Verfahren ist der Ausgabe von Graustufenbildern im Druckwesen nachgebildet und liefert recht ansprechende Ergebnisse.

Dither-Verfahren

Die oben benutzte Dithermatrix versucht die Punkte möglichst gleichmäßig über die Rasterfläche zu verteilen. Die Füllmuster im VDI benutzen diese Methode. Durch die gleichmäßige Verteilung eignet sich dieses Verfahren besonders für Ausgabegeräte mit einer geringen Auflösung, wie etwa dem Bildschirm. Man nennt dieses Verfahren „Dispersed-Dot-Ordered Dither“ (geordnetes Raster mit verteilten Punkten).

Abb. 1: Dispersed-Dot Ordered Dither
Abb. 2: 45-Grad-Clustered-Dot Ordered Dither

Bei einem anderen Verfahren werden die Punkte in Form eines wachsenden Kreises angeordnet. Die unterschiedlichen Helligkeitsstufen erscheinen also als ein mehr oder weniger großer Klecks. Man nennt es „Clustered-Dot-Orderd Dither", da die Punkte zu einem zusammengefaßt werden. Da dieses Verfahren recht grobe Muster erzeugt, eignet es sich nur für Ausgabegeräte mit einer hohen Auflösung und insbesondere für Nadeldrucker, wie wir gleich sehen werden.

Um eine horizontale Zeilenbildung zu vermeiden, die den Helligkeitseindruck des Auges stören würde, wird ein solches Raster meist entlang einer 45°-Linie ausgegeben. Die Rasterflächen sind also gegeneinander verschoben.

Punkte auf dem Drucker

Wenn wir jetzt eine Grafik mit diesem Verfahren direkt auf dem Drucker ausgeben, am besten noch mit einer Auflösung von 360dpi, gibt es meist ein böses Erwachen: Das Bild ist zu dunkel, ja fast völlig schwarz. Woran liegt das?

Nun, beim Nadeldrucker sind zwar die Abstände zwischen den Punkten jeweils 1/360-Zoll, die Punkte selber jedoch sind viel größer. Ein Blick in das Druckerhandbuch offenbart 1/127-Zoll für einen Druckpunkt bei 24-Nadeldruckern.

Wie wir wissen, wird die Graustufe aus der Menge der schwarzen Punkte pro Rasterfläche bestimmt. Das hat auf dem Bildschirm prima funktioniert, da hier die Größe der Punkte gleich dem Abstand zwischen ihnen ist. Der Drucker färbt jetzt jedoch mit einem Punkt auch noch einen großen Teil der Umgebung schwarz.

Bei einem Punktabstand von 1/360 Zoll ist so schon die ganze Fläche schwarz, wenn nur jeder dritte Punkt gesetzt ist, da die Überschneidungen schon die eigentlich weißen Flächen mit abdecken. Wenn also die Punkte gleichmäßig in der Rasterfläche verteilt sind, wird die mögliche Auflösung durch die Punktgröße bestimmt, ist also etwa 127dpi. Etwa, da die Punkte keine Rechtecke sondern Kreise sind, also nie exakt in unsere rechteckige Fläche passen.

Soll das jetzt heißen, daß man aus der Auflösung von 360dpi keinen Nutzen für die Rasterdarstellung ziehen kann? Nein, wir müssen nur die ungewollten Überschneidungen möglichst minimieren, indem wir z.B. die Punkte dicht nebeneinander setzen, so daß die meisten Überschneidungen also mit bereits gesetzten Punkten auftreten. Wir würden so einen sich vergrößernden Klecks erhalten. Wir benutzen also die Clustered-Dot-Ordered-Dither-Matrix. Um festzustellen, wie groß die schwarze Fläche bei einem bestimmten Rastermuster tatsächlich ist, müßte man jetzt nur noch die Überschneidungen bei vorgegebenen Punktgrößen und Abständen berechnen.

Abb. 3: Auflösung eines 24-Nadeldruckers bei 360dpi

Der Druckersimulator

Derartige Berechnungen wären natürlich fürchterlich kompliziert, also lassen wir die Arbeit vom Computer erledigen. Das Programm PRTSIMUL.C simuliert nun einfach die Ausgabe eines Druckers. Hierzu wird einfach die Rasterfläche maßstabsgetreu auf dem Bildschirm dargestellt. Die Druckpunkte werden durch Kreise mit dem entsprechenden Radius repräsentiert. Die geschwärzte Fläche wird dann einfach über die Anzahl der schwarzen Bildschirm-Pixel bestimmt. Als Eingabeparameter braucht das Programm den Namen der Datei, in der die Dithermatrix steht, die Punktabstände in X- und Y-Richtung, die Größe eines Punktes und die Datei, in der die berechnete Matrix für den Drucker abgelegt wird. Hierbei kann die Angabe der Punktgröße und der Abstände wahlweise in dpi oderMillimetererfolgen. Die Eingaben werden dann in Millimeter umgerechnet. Nach Bestätigung der Eingaben erfolgt die eigentliche Simulation. Während dieser werden oben links auf dem Bildschirm die bearbeitete Graustufe und der errechnete Schwellwert angezeigt. Die Simulation läßt sich durch Drücken beider Maustasten abbrechen. An Ende der Simulation wird die so errechnete Schwellwertmatrix in der vorher angegebenen Datei abgespeichert.

Die Kreise werden nicht mit der Kreisfunktion erzeugt, da diese optisch richtige Kreise ausgibt, also bei Auflösungen mit unterschiedlichen Horizontal- und Vertikalauflösungen eigentlich eine Ellipse zeichnet, die dann wie ein Kreis aussieht. Da für die Auszählung der Punkte aber „physikalische“ Kreise benötigt werden, verwendet das Programm die Ellipsenfunktion mit gleichem X- und Y-Radius.

Abb. 4: Bild mit GREY2IMG für 360dpi 24-Nadeldrucker erzeugt

Das Programm ist nicht sehr komfortabel und auch nicht besonders sauber geschrieben, werden z.B. Ausgaben über das VDI und über GEMDOS getätigt. Es sollte jedoch in allen Auflösungen lauffähig sein. Eine saubere Programmierung hätte den Rahmen eines Artikels gesprengt. Das Format der Ausgabedatei wird durch die Struktur „RASTER" bestimmt und hat folgenden Aufbau:

typedef struct
{
    int pix_w; 
    int pix_h; 
    int w; 
    int h;
    unsigned char matrix[1024];
} RASTER;

Hierbei sind pix_w und pix_h die Abstände zwischen den Punkten in Millimetern mal 1000. w und h sind die Breite und Höhe der Rasterfiäche und in den ersten w*h-Bytes sind dann Zeile für Zeile die Schwellwerte abgespeichert, wobei ein großer Schwellwert für eine hohe Schwärzung steht. Das ist zwar genau das Gegenteil der üblichen Beschreibung von Grauwertbildern, wo eine große Zahl eine hohe Helligkeit bedeutet, aber im Umgang mit Druckern einleuchtender, da ja schließlich schwarze Punkte gesetzt werden.

Die Matrixdatei

Die Eingabedatei legt die Dither-Matrix fest und ist, um sie leicht variieren zu können, eine Textdatei. Diese Textdatei hat folgenden Aufbau: Zuerst stehen zwei Zahlen, die die Breite und Höhe der Matrix angeben. Die maximale Ausdehnung beträgt 32x32 Punkte. Danach folgt die eigentliche Matrix. Hier werden keine Schwellwerte angegeben, sondern die Reihenfolge, in der die Punkte in der Rasterfläche gesetzt werden sollen. Der größte Wert ist also gleich der Rasterbreite mal der Rasterhöhe und der kleinste Wert ist eins.

Ein Beispiel. Wenn wir unsere oben verwendete 2x2-Matrix als Eingabedatei formulieren wollten, sähe die Datei etwa so aus:

2,2 Breite und Höhe der Matrix
1,3 erste Zeile
4,2 zweite Zeile

Die Trennung zwischen den Zahlen kann jedes Zeichen oderjede Zeichenfolge sein, die keine Zahlen sind. Kommentare können beliebig eingefügt werden, dürfen nur keine Zahlen enthalten. PATTERN.DAT ist eine Beispieldatei für eine 8x8-Clustered-Dot-Ordered-Dither-Matrix.

Der Bildermacher

Was für einen Wert hat jedoch eine Druckersimulation, wenn es kein Programm gibt, das frei definierbare Matrizen zur Ausgabe unterstützt? Also schreiben wir uns eins.

Das Programm GREY2IMG.C ist ein TTP-Programm, das aus einem Graustufenbild ein GEM-Image für den Drucker erzeugt. Hierzu bedient es sich der vom Druckersimulator berechneten Matrizen. Die erzeugten Images können dann z.B. in Script-Dokumente eingebunden und ausgedruckt werden.

Das Programm ist ein TTP-Programm und erwartet über die Kommandozeile wenigstens den Namen des Quell- und des Zielbildes. Zusätzlich können noch einige Einstellungen getätigt werden. Zum Format der Kommandozeile im einzelnen:

Parameter-Erklärung

-I Ist dieser Parameter eingestellt, wird ein Bild mit der Matrix für niedrige Auflösung erzeugt.

-h Ist dieser Parameter eingestellt, wird ein Bild mit der Matrix für hohe Auflösung erzeugt. Standardmäßig ist -I eingestellt.

-s ist dieser Parameter angegeben, wird das Raster um 45' verschoben. Standardmäßig aus.

-2 Ist als Parameter eine Zahl von eins bis neun angegeben, wird sie als Vergrößerungsfaktor interpretiert. Das Bild wird dann in X- und Y-Richtung um diesen Faktor vergrößert. Standard ist Eins.

infile Der Dateiname des Grauwertbildes. Das Bild muß im B&W256-Format vorliegen.

outfile Der Dateiname des Ausgabebildes. Es wird ein Bild im GEM-Image-Format erzeugt.

Das Programm sucht nach dem Start nach den Dateien „HI.PAT“ und „LO.PAT" im aktuellen Verzeichnis. Sollten sie vorhanden sein, werden sie geladen und ersetzen die Standardeinstellung. Hierbei wird nicht überprüft, ob die Dateien das richtige Format haben. Solche Dateien können vom Druckersimulator erzeugt werden. Standardmäßig sind Matrizen für 24-Nadeldrucker mit 180dpi (-I) und 360dpi (-h) eingestellt.

Das B&W256-Format besteht aus einem Kopf und den eigentlichen Bilddaten. Die ersten sechs Bytes des Kopfes sind „B&W256“. Dann folgen die Breite des Bildes (zwei Bytes) und die Höhe des Bildes (zwei Bytes). Die Bilddaten sind als Byte-Werte Zeile für Zeile abgespeichert, wobei 0 Schwarz und 255 Weiß bedeutet.

Bilder dieses Formates werden beispielsweise von dem Programm IMAGELAB erzeugt. Es ist ein sehr einfaches aber dennoch flexibles Format, eignet sich also hervorragend für unsere Zwecke.

Das erzeugte Bild sieht auf dem Bildschirm betrachtet natürlich zu hell und kontrastarm aus. Wenn es allerdings direkt auf dem Drucker ausgegeben wird, hat es eine überaus ansprechende Qualität. Wenn es also z.B. in Script geladen wird, muß nur noch die Auflösung für den Ausdruck eingestellt werden.

Das Programm ist recht langsam. Das ist der Preis für die hohe Flexibilität und den kurzen Programmcode. Dennoch spart es viel Zeit, da es das Hantieren mit komplizierten Aufhellungsverfahren erspart.

Die Programme...

... sind vollständig in Pure C geschrieben und sollten ohne Probleme mit anderen C-Compilern harmonieren. Zu beachten ist nur, daß GREY2IMG ein TTP-Programm ist, also mit der entsprechenden Endung zu versehen ist.

Der Druckersimulator ist ausgesprochen multitasking-feindlich, da er VDI-Ausgaben direkt auf den Bildschirm tätigt. Er sollte als PRG-Datei gestartet werden.

Zum Abschluß

Natürlich ist die Qualität des Bildes noch immer nicht perfekt, doch was ist das schon? Die Größe eines Druckpunktes hängt nicht nur von der Nadeldicke ab, es spielen auch noch Papierdicke und Farbband eine Rolle. Auch ist Papier nicht völlig weiß. Diese Faktoren sind jedoch so gering, daß sie sich nicht allzu wesentlich auf den Ausdruck auswirken. Es war auch nicht mein Ziel, sofort perfekte Ergebnisse zu erzielen. Es reicht mir, wenn das Ergebnis sofort brauchbar ist. Weitere Feinjustierungen müssen nach wie vor von Hand durchgeführt werden.

Moderne Drucker bieten meist von sich aus bereits Möglichkeiten zur Rasterung von Bildern an. Nur werden diese noch von den wenigsten Programmen unterstützt. Image-Bilder können allerdings von fast allen Programmen ausgegeben werden. So hat das hier vorgestellte Verfahren auch für solche Drucker seine Berechtigung.

Ich würde mir wünschen, daß Programme, die Grafiken ausdrucken können, in Zukunft frei definierbare Dither-Matrizen unterstützen und so dem Anwender eine Menge Ärger ersparen.

Literaturhinweis:
Haberäcker, Peter.
Digitale Bildverarbeitung:
Grundlagen und Anwendungen. München Wien 1991.
Loviscach, Jörn. Fotolabor -
Grundlegendes zur Bildbearbeitung, c’t 9/94.


/*
    PRTSIMUL.C
    Druckersimulator zur Mustergenerierung 
    (c)1995 MAXON Computer 
    Autor: Thomas Schütt
*/

#include <vdi.h>
#include <aes.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>

typedef struct
{
    int pix_w; 
    int pix_h; 
    int w; 
    int h;
    unsigned char matrix[1024];
} RASTER;

int handle, work_in[11], work_out[57];

RASTER raster;
double x_dis, y_dis, siz;
char infile[128], outfile[128];

unsigned char matrix[1024];

int mat_w, mat_h;

double finput (const char *s)
{
    double v;

    do
    {
        printf ("\r%s", s); 
        scanf ("%lf", &v);
    } while (v == 0.0);

    if (v == floor (v))
        v = 25.7 / v; /* dpi -> mm */ 
    return (v);
}/* finput */

void input (void)
{
    char jn[2]; 
    do {
        printf ("\33E\33eDruckersimulation " 
                "1.0\n\n\r"); 
        printf ("Eingabedatei (Matrix): "); 
        scanf ("%s", infile);
        x_dis = finput ("\nPunktabstand (X) [mm]" 
                        " oder [dpi]:”); 
        y_dis = finput ("Punktabstand (Y) [mm] " 
                        "oder [dpi]:"); 
        siz = finput ("Punktgroße [mm] oder "
                      "[dpi]      :");
        printf ("Ausgabedatei (Matrix): "); 
        scanf ("%s", outfile); 
        printf ("\33EAusgangswerte\n\n\r"); 
        printf ("Punktabstand (X) %.41fmm\n\r", x_dis);
        printf ("Punktabstand (Y) %.41fmm\n\r", y_dis);
        printf ("Punktgröße %.41fmm\n\r", siz); 
        printf ("Eingabedatei: '%s'\n\r", infile); 
        printf ("Ausgabedatei: '%s'"
                "\n\n\rEingaben ok? [j/n]", 
                outfile); 
        scanf ("%ls", jn);
    } while (jn[0] != 'j' && jn[0] != 'J');
    printf ("\33E\33f");
}/* input */

int getnumber (FILE *file)
{
    char buffer[20]; 
    int c, i;

    do
    {
        if ((c = fgetc (file)) == EOF) 
            return (-1);
    } while (!isdigit (c)); 
    i = 0; 
    do 
    {
        buffer[i++] = c; 
        c = fgetc (file);
    } while (isdigit (c)); 
    buffer[i] = '\0'; 
    return (atoi (buffer));
}/* getnumber */

int read_matrix (const char *fnamej
{
    unsigned char *inptr; 
    int count, n;
    FILE *file;

    if ((file = fopen (fname, "r")) == null) 
        return (0); 
    if ((mat_w = getnumber (file)) < 0) 
        return (-1); 
    if ((mat_h = getnumber (file)) < 0) 
        return (-1);

    inptr = matrix;
    for (count=mat_w*mat_h; count>0; count--) 
        if ((n = getnumber (file)) >= 0) 
            *inptr++ = n;
        else
            return (-1); 
    fclose (file); 
    return (1);
}/* read_matrix */

int write_pattern (const char *fname)
{
    FILE *file;

    raster.pix_w = x_dis * 1000.0; 
    raster.pix_h = y_dis * 1000.0; 
    raster.w = mat_w; 
    raster.h = mat_h;
    if ((file = fopen (fname, "wb")) == NULL) 
        return (0); 
    if (fwrite (&raster, sizeof (RASTER), 1, file) < 1) 
        return (0); 
    fclose (file); 
    return (1);
}/* write_pattern */

void prtsimul (void)
{
    int cx, cy, cw, ch, xd, yd, xp, yp, r, fac, 
        pxy[8], i, x, y, pel, d, button, new, 
        nx, ny; 
    long maxsum, sum;

    fac = 150.0 / ((mat_h + 1) * y_dis);
    xd = x_dis * fac;
    yd = y_dis * fac;
    r = siz * fac / 2;
    cw = xd * mat_w;
    ch = yd * mat_h;
    cx = (work_out[0] - cw) / 2;
    cy = (work_out[1] - ch) / 2;
    v_clrwk (handle);
    vsf_perimeter (handle, 0);
    pxy[0] = cx; 
    pxy[1] = cy;
    pxy[2] = cx + cw - 1; 
    pxy[3] = cy + ch - 1;
    vs_clip (handle, 1, pxy); 
    xp = cx + xd / 2; 
    yp = cy + yd / 2; 
    maxsum = cw * ch;

    for (i=1; i<=mat_w*mat_h; i++)
    {
        for (y=0; y<mat_h; y++)
            for (x=0; x<mat_w; x++)
                if (i == matrix[y*mat_w+x])
                {
                    nx = x; 
                    ny = y;
                    v_ellipse (handle, xp+xd*x, yp+yd*y, r, r) ;
                }/* if, for, for */ 
            sum = 0L;
            for (y=cy; y<cy+ch; y++)
                for (x=cx; x<cx+cw; x++)
                {
                    v_get_pixel (handle, x, y, &pel, &d); 
                    vq_mouse (handle, abutton, &d, &d);
                    if (pel) 
                        sum++; 
                    if (button == 3) 
                        exit (1);
                }/* for, for */ 
                new = (double) sum / (double) maxsum * 255.0 + 0.5; 
                raster.matrix[ny*mat_w+nx] = new; 
                printf ("\33HGraustufe %d: %d", i, new);
    }/* for */
}/* prtsimul */

int main ()
{
    int d, i, ok; 

    appl_init ();

    handle = graf_handle (&d, &d, &d, &d); 

    for (i=0; i<10; work_in[i++] = 1) ;

    work_in[10] = 2;

    v_opnvwk (work_in, &handle, work_out);

    v_hide_c (handle); 
    v_clrwk (handle); 
    input ();
    ok = read_matrix (infile); 
    if (ok <= 0)
    {
        if (ok == 0)
            puts ("\rKann Eingabedatei "
                  "nicht finden.");
        else
            puts ("\rFehler in Eingabedatei."); 
        return (0);
    }/* if */ 
    prtsimul ();
    if (!write_pattern (outfile))
        puts ("\rFehler beim schreiben.");

    v_show_c (handle, 0); 
    v_clsvwk (handle); 
    appl_exit (); 
    return (0);
}/* main */


Thomas Schütt
Aus: ST-Computer 04 / 1995, Seite 110

Links

Copyright-Bestimmungen: siehe Über diese Seite