Contents

%t_Halftone - Printing tutorial
%
%PURPOSE:  Image halftoning for binary, black/white, printers
%AUTHOR:   R. Koehler, X. Zhang, B. Wandell
%DATE:     02.25.97
%CONCEPTS:
%
% This tutorial illustrates how to use halftoning to achieve the
% visual illusion of gray-levels in a binary display system.
% You will learn about two approaches:  thresholding approaches
% using dither patterns and the computational method called error
% diffusion.
%
% Matlab 7:  Checked 01.08.06
% Matlab 5:  Checked 01.06.98
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Most printers place a binary mark at each spot on the page:
% that is, they either place a dot at position or not. Hence,
% unlike monitors, printers do not display a range of intensity
% levels.

% To create the illusion of intensity variation, printers fool
% the eye by trading area for intensity.  One way to do this is
% to divide the image into a number of small areas (halftone
% cells) and print more or less dots in this area according to a
% rule that is governed by the local image intensity.  This
% halftoning process substitutes a number of dots for a variation
% in gray level (or color).

% The algorithm for simple halftoning is to define a small
% pattern, defined over the halftone cell, that spans a small
% number of printable dots.  The values within this halftone cell
% define a set of thresholds.  The binary decision of whether to
% print or not at each addressable print location is governed by
% how the image intensity compares to the entry in the halftone
% cell.  Halftone algorithms are often named by the principle
% that underlies the pattern within the halftone cell.

% Let's begin with a simple halftone algorithm called "cluster dot."
% This algorithm uses a halftone cell in which the printed area
% has the shape of a larger and larger dot.  As the image region
% within the halftone cell becomes darker and darker, the size of
% the dot grows larger.  Here is an example of a 4x4 halftone
% cell that illustrates the idea.

halfToneCell_4 = ...
    [15 5 12 14
    10 3 2 8
    7 1 4 11
    13 9 6 16];

% Suppose we have a 4x4 image region that has a hight density,
% say 12.
im = 12*ones(4,4);

% To print this section of the image, we compare the density in
% the image with the thresholds in the halftone cell,
cmp = halfToneCell_4 < im

%  The locations set to one are printed with a black dot, and the
% locations with 0 are unprinted (white).  Because of Matlab's
% ordering, we will add 1 to cmp and build the binary color map
% as
bMap = [1 1 1; 0 0 0]

figure;
colormap(bMap); image( cmp + 1 );

% For this high density, we print most of the halftone cell as
% black.  Now, let's see how the halftone cell would print at a
% set of increasing densities:

cnt = 1;
for density = 1:16
    im = ones(4,4)*density;
    cmp = (halfToneCell_4 < im);
    subplot(4,4,cnt)
    image(cmp + 1);
    cnt = cnt + 1;
end

% As the mean image density varies from low (light) to high
% (dark) the region corresponding to the halftone cell is printed
% as an increasingly large black dot.

% Now we can apply the halftone cell to an entire test image.
% We start with a simple test image containingeight gray strips
% that vary from black to white.

grayStrips = 8;
bw_range = [0, 1];
sweep_size = 128;

%
[x,y] = meshgrid(0:sweep_size-1, 0:sweep_size-1);
y = fix(y * grayStrips / sweep_size) / grayStrips;
x = x / (sweep_size * grayStrips);
sweep_8 = ones(sweep_size, sweep_size) - (x + y);

% This ramp pattern has many gray levels arranged in 8 strips.
% The density increases from left to right.
figure;
imshow(sweep_8);

% The values in sweep_8 are frame buffer values that are
% nonlinearly related to intensity.  The halftoning principle
% depends on LINEAR SPATIAL AVERAGING of the dots.  Hence, to
% make the proper decisions about the halftoning, we have to take
% into account the true intensity associated with each level.
% Hence, before halftoning we need to convert the frame buffer
% values to linear gray scale values (gamma correction--review
% the ColorMatching tutorial for why and how to perform gamma
% correction). We use a gamma value = 2.

monitorGamma = 2;
sweep_8_linear = sweep_8 .^ monitorGamma;
HTsweep_4 = HalfToneImage(halfToneCell_4, sweep_8_linear);
imshow(HTsweep_4);

% Were we doing this on a printer, rather than a display, we
% would have to know the relationship between the image density
% and the displayed intensity, and also, we would need to know
% the relationship between the image dot size in the cluster dot
% and the reflected intensity.

% The image halftone is created in the routine
% HalfToneImage. (Use type HalfToneImage to see the full
% routine).  The process simply applies the HalfToneCell
% comparison again and again across the image.  The light image
% regions contain no black dot or a small black dot.  The dark
% image regions contain large black dots or a complete black dot.

type HalfToneImage.m

% The halftone does a worse job tracking the gray levels if the
% halftone cell is smaller.

halfToneCell_2 = [ 4 3; 2 1];
HTsweep_2 = HalfToneImage(halfToneCell_2, sweep_8_linear);
imshow(HTsweep_2);

% We can also scale up these two images to see how the halftones are formed.
%
scale_sweep_2 = kron(HTsweep_2, ones(3,3));
imshow(scale_sweep_2);

scale_sweep_4 = kron(HTsweep_4, ones(3,3));
imshow(scale_sweep_4);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Bayer dithering
%
%%%%%%%%% %%%%%%%%% %%%%%%%%% %%%%%%%%% %%%%%%%%%

% In the supplemental reading, Bayer describes a halftone cell
% for Figure 2: "imitation halftone pattern."  We enter it as:

halfToneCell_B2 = ...
    [7 6 5 16 17 18 19 20
    8 1 4 15 28 29 30 21
    9 2 3 14 27 32 31 22
    10 11 12 13 26 25 24 23
    17 18 19 20 7 6 5 16
    28 29 30 21 8 1 4 15
    27 32 31 22 9 2 3 14
    26 25 24 23 10 11 12 13];

colormap(bMap)
cnt = 1;
for density = 1:32
    im = ones(8,8)*density;
    cmp = (halfToneCell_B2 < im);
    subplot(6,6,cnt)
    image(cmp + 1);
    cnt = cnt + 1;
end

% This pattern is much like the simple cluster dot, except it
% introduces a smaller pair of dots oriented at 45 deg to the
% image. Now apply it to the sweep pattern:
clf
HTsweep_B2 = HalfToneImage(halfToneCell_B2, sweep_8_linear);
imshow(HTsweep_B2);

% Bayer also describes a halftone cell for Figure 4 that
% considered to be much better (he called it an "optimum dot
% pattern").  This pattern is built so that the dots are
% dispersed in a random array.

halfToneCell_B4 = ...
    [1 17 5 21 2 18 6 22
    25 9 29 13 26 10 30 14
    7 23 3 19 8 24 4 20
    31 15 27 11 32 16 28 12
    2 18 6 22 1 17 5 21
    26 10 30 14 25 9 29 13
    8 24 4 20 7 23 3 19
    32 16 28 12 31 15 27 11];

figure;
colormap(bMap)
cnt = 1;
for density = 1:32
    im = ones(8,8)*density;
    cmp = (halfToneCell_B4 < im);
    subplot(6,6,cnt)
    image(cmp + 1);
    cnt = cnt + 1;
end

clf
HTsweep_B4 = HalfToneImage(halfToneCell_B4, sweep_8_linear);
imshow(HTsweep_B4);

% if you want to close the image windows, do this
close all;

% These halftones can also be applied to a real image.

D = load('jpegFiles/einstein.mat'); img = D.X;
img = img / 256;
imshow(img);

% gamma correction
img_linear = img .^ monitorGamma;

HTimg_B4 = HalfToneImage(halfToneCell_B4, img_linear);
imshow(HTimg_B4);

% If you enlarge this image you should see that the halftone
% cells are not the same as the basic patterns we saw.  This is
% because the halftone dot is formed by the space-varying image
% not simple constant patterns. The image changes density rapidly
% over sapce and the dots reflect the correct image density.

scale_img_B4 = kron(HTimg_B4, ones(3,3));
imshow(scale_img_B4);

% You can see this easily by changing the original sweep to
% a different number of strips, say 5.  Thus the halftone
% cells will not line up on the strip boundaries.

grayStrips = 5;
[x,y] = meshgrid(0:sweep_size-1, 0:sweep_size-1);
y = fix(y * grayStrips / sweep_size) / grayStrips;
x = x/(sweep_size * grayStrips);
sweep_5 = ones(sweep_size, sweep_size) - (x + y);
imshow(sweep_5);

% gamma correction
%
sweep_5_linear = sweep_5 .^ monitorGamma;

HTsweep_5_4 = HalfToneImage(halfToneCell_4, sweep_5_linear);
imshow(HTsweep_5_4);

scale_sweep_5_4 = kron(HTsweep_5_4, ones(3,3));
imshow(scale_sweep_5_4);
cmp =

  4×4 logical array

   0   1   0   0
   1   1   1   1
   1   1   1   1
   0   1   1   0


bMap =

     1     1     1
     0     0     0


function htimage = HalfToneImage(cell, im)
%
%            htimage = HalfToneImage(cell, im)
%
% AUTHOR:  Koehler, Zhang, Wandell
% DATE:    02.25.97
% PURPOSE:
%
% This function takes in a halftone cell and an image as
% arguments.
%    cell: An array of threshold levels.  If these exceed 1, then
%          the cell is scaled to evenly spaced values between 0 and 1.
%          If the numbers all fall between 0 and 1, then they are
%          left as they are.
%    im:   The image gray levels, values between 0 and 1.
%
% htimage: The returned halftone image.  Its values are set to 0
% (white) or 1 (black).  The binary color map is [ 1 1 1; 0 0 0];

% DEBUGGING:
% im = (rand(32,32)).^2;
% cell = [ 1 2 ; 3 4];

imSize = size(im);
cellSize = size(cell);

% The cell thresholds should fall at the midpoints of the
%
if max(cell) > 1
    low = (1/max(cell(:)))*0.5; high = 1 - low;
    halfToneCell = ieScale(cell,low,high);
else
    halfToneCell = cell;
end

% Determine number of halftone cells needed to cover the image
%
rc = imSize ./ cellSize;
r = ceil(rc(1));
c = ceil(rc(2));

% Builds an image that covers the original and whose entries
% contain the values of the halfToneCell repeated, again and again.
halfToneMask = kron(ones(r, c), halfToneCell);

% Crop out that part of the mask equal in size to the image.
halfToneMask = halfToneMask(1:imSize(1), 1:imSize(2));

% Compare the image intensity at each point to the value in the
% halftone mask.  If the image density exceeds the mask we use
% a 1, otherwise a 0.
htimage = (halfToneMask < im);

return;

% DEBUGGING
% colormap([1 1 1; 0 0 0])
% subplot(1,2,1)
% imagesc(im), axis image
% subplot(1,2,2)
% imagesc(htimage), axis image

close all image windows

close all;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Error Diffusion
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% This version uses an algorithm by Floyd and Steinberg
% which is described in the course supplemental
% reading.  The algorithm is called error diffusion, though
% it is sometimes called stochastic screening.  Variations
% of the basic error diffusion pattern are used by Ulichney.

% Unlike the halftone cells described in part 1, error diffusion
% does not use a fixed positioning for the halftone output.
% Rather, each pixel is compared to a threshold and a decision
% is made to print that pixel as black, or leave it white.
% The difference between the actual pixel density and the level
% printed is the error.  That error is diffused to neighboring
% pixels (to the right and/or down) and the process is repeated.
% We can use the same images as we did before to see the impact
% of this type of halftoning.  Actually, this process is usually
% referred to as dithering.  Screening is yet another term which
% includes both the halftoning described in tutorial part 1 and
% here.  (Dithering can also refer to the halftoning previously
% described.)

% As with the fixed position cells described before, the objective
% of error diffusion is to replace a set of intermediate level
% densities with a binary set whose area approximates the optical
% denisty (appearance) of the original.

% The algorithm has one free set of parameters called the
% diffusion matrix, FS.  This matrix determines how the error at
% a point is distributed to its neighbors.  The pixel being
% considered is in the middle of the top row of FS.  The fraction
% of the error to be applied to each neighbor is set by the value
% in FS.  There is no standard way to deal with the errors that
% are accumulated at the end of each row: some peple just throw
% them away, others roll them down to the next row, others - who
% knows.  Here, we will treat the image as if it were a spiral
% and roll the errors from each end "down and to the right" or
% "up and to the left."  This is complicated in the algorithm so
% don't worry too much if the process is not entirely clear.

% The simplest error diffusion is to just pass the error from
% each pixel to the neighbor to the right.  We can do this with:

FS_1 = [0 0 1];

% You can view the implementation of the error diffusion by
% entering: type FloydSteinberg.m

HT_image_1 = FloydSteinberg(FS_1, sweep_8_linear);

imshow(sweep_8);
imshow(HT_image_1);

% Now we can apply the matrix specified by Floyd and Steinberg
% in their paper.

FS_2 =	[0 0 0 7 5; ...
    3 5 7 5 3; ...
    1 3 5 3 1];
FS_2 = FS_2 / sum(FS_2(:));
HT_image_2 = FloydSteinberg(FS_2, sweep_8_linear);
imshow(HT_image_2);

% And we can pick one from Ulichney
%

FS_3 =	[0 0 7;  ...
    3 5 1];
FS_3 = FS_3 / sum(FS_3(:));
HT_image_3 = FloydSteinberg(FS_3, sweep_8_linear);
imshow(HT_image_3);

% Finally, let's look at a real image
%
D = load('jpegFiles/einstein.mat'); img = D.X;
img = img / 256;
imshow(img);
img_linear = img .^ monitorGamma;
HTimg_2 = FloydSteinberg(FS_2, img_linear);
imshow(HTimg_2);

% People's preference depends on the choice of output device.
% Specific printing technologies have different properties that
% make different halftoning algorithms look better or worse.
% Choosing a screening process can be quite difficult because
% methods of deciding on the preference between two images,
% neither of which is very good, is hard to specify.  Worse yet
% even when people do make consistent choices the choice they
% make can depend on the test image.  Sigh.


%%%%%%%END