There are numerous methods for computing a grayscale image from a color image. One of the simplest is to average all three color channels:
(R + G + B)/3
This is identical to applying a weight factor of 1/3 to each of the color channels:
1/3R + 1/3G + 1/3B
However, this method tends to produce muddy images, mainly because the human eye responds to different frequencies of light differently. A better method involves adjusting the weightings to match the perceived luminosity of each color channel. There are several models for this. A commonly cited model places these values at 0.2126 for red, 0.7152 for green, and 0.0722 for blue.
0.2126R + 0.7152G + 0.0722B
Of course, computations of this form assume a linear response curve for both the human eye and the display device, neither of which is correct. Indeed, an accurate and precise model is likely to be compuationally very expensive - which brings me to the purpose of this article...
For computational efficiency, it is imperative to avoid floating point arithmetic, and generally avoid expensive integer computations such as multiply and divide. Remembering that multiplying and dividing by powers of two can be replicated using shift operations, the following weightings can be applied which are reasonably close to the luminosity model:
0.25R + 0.625G + 0.125B or (2R + 5G + B)/8
This can be computed as follows:
((R shl 1) + (G shl 2) + G + B) shr 3
Converting this to practical assembly, assuming ESI is a 24 bit source image, and EDI is an 8 bit destination image:
mov AX,[ESI]
movzx DX,AH //DX = Green
xor AH,AH //AX = Blue
add AX,DX
shl DX,2
add AX,DX
movzx DX,[ESI+2] //DX = Red
shl DX,1
add AX,DX
shr AX,3
mov [EDI],AL
Note that if you desire an in-place conversion, EDI is not necessary, and the last line can be replaced with:
mov [ESI],AL
mov AH,AL
mov [ESI+1],AX
A complete Delphi implementation of the converter:
procedure ColorToGray(BMP: TBitmap);
var
Y: integer;
pSrc: PRGBTriple;
pDest: PByte;
W: longword;
Work: TBitmap;
begin
BMP.PixelFormat := pf24Bit;
W := BMP.Width;
Work := TBitmap.Create;
try
Work.PixelFormat := pf8Bit;
Work.Palette := GetGrayPalette;
Work.SetSize(W, BMP.Height);
for Y := 0 to BMP.Height-1 do
begin
pSrc := BMP.ScanLine[Y];
pDest := Work.ScanLine[Y];
asm
push EDI
push ESI
mov ESI,pSrc
mov EDI,pDest
mov ECX,W
@@Loop:
mov AX,[ESI]
movzx DX,AH
xor AH,AH
add AX,DX
shl DX,2
add AX,DX
movzx DX,[ESI+2]
shl DX,1
add AX,DX
shr AX,3
mov [EDI],AL
add ESI,3
inc EDI
dec ECX
jnz @@Loop
pop ESI
pop EDI
end;
end;
BMP.assign(Work);
finally
Work.Free;
end;
end;
Sometimes you don't need color, and single channel image data is enough. This is frequently true of document imaging and some types of photography like thermal and infrared. Unfortunately, Windows does not natively support one-channel images, and so a grayscale palette must be constructed to represent the image.
The following functions will produce grayscale palettes for the appropriate pixel format. The first produces the full 8-bit spectrum:
function GetGrayPalette: HPALETTE;
var
pPal: PLogPalette;
begin
GetMem(pPal,1028);
try
asm
push EDI
mov EDI,pPal
mov [EDI],$300
mov ECX,$100
mov [EDI+2],CX
mov EDX,$010101
xor EAX,EAX
@@Loop:
add EDI,4
mov [EDI],EAX
add EAX,EDX
dec ECX
jnz @@Loop
pop EDI
end;
result:=CreatePalette(pPal^);
finally
FreeMem(pPal);
end;
end;
An evenly distributed 4-bit palette:
function GetGrayPalette16: HPALETTE;
var
pPal: PLogPalette;
begin
GetMem(pPal,68);
try
asm
push EDI
mov EDI,pPal
mov [EDI],$300
mov ECX,$10
mov [EDI+2],CX
mov EDX,$111111
xor EAX,EAX
@@Loop:
add EDI,4
mov [EDI],EAX
add EAX,EDX
dec ECX
jnz @@Loop
pop EDI
end;
result:=CreatePalette(pPal^);
finally
FreeMem(pPal);
end;
end;
Bi-tonal images typically do not require a palette as it assumed on most devices that 0 = black and 1 = white. However, for completeness, the 1-bit black and white version:
function GetBWPalette: HPALETTE;
var
pPal: PLogPalette;
begin
GetMem(pPal,12);
try
asm
mov EAX,pPal
mov [EAX],$20300
mov [EAX+4],longword(0)
mov [EAX+8],$FFFFFF
end;
result:=CreatePalette(pPal^);
finally
FreeMem(pPal);
end;
end;