2007. március 30., péntek

Graying Bitmaps and Graphics


Problem/Question/Abstract:

This article shows how to gray a color bitmap. It reduces true-color bitmaps to 256 shades of gray paletted bitmaps, which reduces memory requirements.
The article also provides a function for graying any TGraphic descendant which is assignable to a TBitmap (or who knows how to assign itself to one: AssignTo method)

Answer:

After a while without posting I've came up with this article which explores a simple yet (non)colorful subject: changing color images to gray scale.

Writting a function to do that is easy: one could simply get a bitmap, promote it to 32 or 24 bit true-color, and then get the pixel components, one by one, and change them to the arithmetic avarage (red+green+blue component div 3) for every pixel. Something like:

Bitmap.PixelFormat := pf24Bit;
for y := 0 to Bitmap.Height - 1 do
  for x := 0 to Bitmap.Width div 3 do
  begin
    C := PChar(ScanLine[y])[x * 3] + PChar(ScanLine[y])[x * 3 + 1] +
      PChar(ScanLine[y])[x * 3 + 2] div 3;
    FillChar(PChar(SCanLine[y])[x * 3], 3, C);
  end;

And you will get a grayed bitmap wich is stored as a 24 bit depth true-color picture. What a wast of space and memory... (Attention: I didn't tested the above code, it is much more an algorithm than an implementation... I've written it directly here while writting the article :-)

But using this technique is not a good approach! First, every grayscale image can only have 256 shades of gray in current Windows based computers, since the Red, Green and Blue component each can only vary from 0 to 255. A gray scale image is one where R=G=B, so there can only be 256 possible levels of gray (or intensity). So using true color images to store a gray one is waste of space.

The code bellow in an excerpt from my work on progress DGL (Delphi Graphics Library), which I think I will never finish due to my load on work and at home (I am a Jiu-Jitsu fighter and have to attend to the trainning every day!!!! :-). This code was encapsulated in one filter class (TGrayFilter), because the DGL uses filters to apply effects and transformations on images. Here I've stripped the object orientation completely and wrote two simple functions to do it for you.

It is supposed that you have some familiarity with Bitmap scanlines to fully understand what is going on, and with the methods I use here to manipulate Scanlines. If you didn't have that knowledge, you could take a look at my article "BitmapToRegion (Delphi-like version - very fast) (UPDATE: Bug fix!)", Article # 944. There I enter in more detail about Scanlines and the methods I will use here.


The project bellow is very simple. To test it all you need to do is to save the DFM (which I suplly in text format) by copying and pasting in Notepad and saving the file as Unit1.dfm. After that open the form in Delphi and copy and past the code bellow in the entire unit. After that add this unit to a project and run it.

---- CODE -----

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, JPEG,
  ExtDlgs, StdCtrls, Buttons, ExtCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    btnOpen: TBitBtn;
    OpenPic: TOpenPictureDialog;
    btnGrayBitmap: TBitBtn;
    btnGrayGraphic: TBitBtn;
    procedure btnOpenClick(Sender: TObject);
    procedure btnGrayBitmapClick(Sender: TObject);
    procedure btnGrayGraphicClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

{ general routines - They are in a separate unit in my on progress Delphi
  Graphics Library, but for this example I will put them here }

procedure GetScanLineProperties(Bitmap: TBitmap; var Start: Pointer;
  var Dif: Integer);
begin
  Start := Bitmap.ScanLine[0];
  if Bitmap.Height > 1 then
    Dif := Integer(Bitmap.ScanLine[1]) - Integer(Start)
  else
    Dif := 0;
end;

function BuildGrayPalette(PixelFormat: TPixelFormat): HPalette;
var
  Pal: TMaxLogPalette;
  i, step: Integer;
  C: Integer;
begin
  Pal.palVersion := $300;
  step := 1;
  case PixelFormat of
    pf1bit: step := 255;
    pf4bit: step := 16;
    pf8bit: step := 1;
  end;
  if step < 255 then
    Pal.palNumEntries := 256 div step
  else
    Pal.palNumEntries := 2;
  if PixelFormat = pf4bit then
  begin
    C := step - 1;
    for i := 0 to Pal.palNumEntries - 1 do
    begin
      FillChar(Pal.palPalEntry[i], 3, C);
      Pal.palPalEntry[i].peFlags := 0;
      Inc(C, step);
    end;
  end
  else
  begin
    C := 0;
    for i := 0 to Pal.palNumEntries - 1 do
    begin
      FillChar(Pal.palPalEntry[i], 3, C);
      Pal.palPalEntry[i].peFlags := 0;
      Inc(C, step);
    end;
  end;
  Result := CreatePalette(PLogPalette(@Pal)^);
end;

function GrayPaletteEntries(Pal: HPALETTE): HPALETTE;
var
  PaletteSize: Cardinal;
  LogPal: TMaxLogPalette;
  i: Integer;
begin
  Result := 0;
  if Pal = 0 then
    Exit;
  PaletteSize := 0;
  if GetObject(Pal, SizeOf(PaletteSize), @PaletteSize) = 0 then
    Exit;
  if PaletteSize = 0 then
    Exit;
  with LogPal do
  begin
    palVersion := $0300;
    palNumEntries := PaletteSize;
    GetPaletteEntries(Pal, 0, PaletteSize, palPalEntry);
    for i := 0 to palNumEntries - 1 do
      FillChar(palPalEntry[i], 3, (palPalEntry[i].peRed +
        palPalEntry[i].peGreen +
        palPalEntry[i].peBlue) div 3);
  end;
  Result := CreatePalette(PLogPalette(@LogPal)^);
end;

procedure GrayBitmap(Bitmap: TBitmap);
var
  Dest: TBitmap;
  SrcRow, DstRow: PByteArray;
  DstDif, SrcDif, x, y, bpp: Integer;
begin
  GetScanLineProperties(Bitmap, Pointer(SrcRow), SrcDif);
  case Bitmap.PixelFormat of
    { palette - need only to gray the palette entries }
    pf1Bit, pf4Bit, pf8Bit:
      begin
        Bitmap.Palette := GrayPaletteEntries(Bitmap.Palette);
      end;
    { true color - will reduce to 8-bit palette (slower but saves memory) }
    pf15Bit, pf16Bit:
      begin
        raise
          Exception.Create('Not implemented! I am tired! Try promoting the bitmap to pf24/32bit before calling the function!');
      end;
    pf24Bit, pf32Bit:
      begin
        Dest := TBitmap.Create;
        try
          Dest.PixelFormat := pf8Bit;
          Dest.Width := Bitmap.Width;
          Dest.Height := Bitmap.Height;
          Dest.Palette := BuildGrayPalette(pf8bit);
          GetScanLineProperties(Dest, Pointer(DstRow), DstDif);
          if Bitmap.PixelFormat = pf24bit then
            bpp := 3
          else
            bpp := 4;
          for y := 0 to Pred(Bitmap.Height) do
          begin
            for x := 0 to Pred(Bitmap.Width) do
              DstRow[x] := (SrcRow[x * bpp] + SrcRow[x * bpp + 1] + SrcRow[x * bpp +
                2]) div 3;
            Inc(Integer(SrcRow), SrcDif);
            Inc(Integer(DstRow), DstDif);
          end;
          Bitmap.Assign(Dest);
        finally
          Dest.Free;
        end;
      end;
  end;
end;

procedure GrayGraphic(Graphic: TGraphic);
var
  Work: TBitmap;
begin
  Work := TBitmap.Create;
  try
    // the majority of TGraphic class knows how to assign itself to bitmaps (method AssignTo)
    Work.Assign(Graphic);
    if Work.PixelFormat in [pf15Bit, pf16Bit] then
      Work.PixelFormat := pf32Bit; // 32-bit bitmaps are the fastest true color
    GrayBitmap(Work);
    Graphic.Assign(Work);
    Graphic.Modified := True;
  finally
    Work.Free;
  end;
end;

{ TForm1 }

procedure TForm1.btnOpenClick(Sender: TObject);
begin
  if OpenPic.Execute then
    Image1.Picture.LoadFromFile(OpenPic.FileName);
end;

procedure TForm1.btnGrayBitmapClick(Sender: TObject);
begin
  GrayBitmap(Image1.Picture.Graphic as TBitmap);
end;

procedure TForm1.btnGrayGraphicClick(Sender: TObject);
begin
  GrayGraphic(Image1.Picture.Graphic);
end;

end.

---- FORM AS TEXT ----- COPY AND PAST IT TO NOTEPAD AND SAVE AS UNIT1.DFM -----

object Form1: TForm1
  Left = 290
    Top = 129
    Width = 696
    Height = 480
    Caption = 'Form1'
    Color = clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name = 'MS Sans Serif'
    Font.Style = []
    OldCreateOrder = False
    PixelsPerInch = 96
    TextHeight = 13
    object Image1: TImage
    Left = 88
      Top = 8
      Width = 225
      Height = 209
      AutoSize = True
  end
  object btnOpen: TBitBtn
    Left = 8
      Top = 8
      Width = 75
      Height = 25
      Caption = '&Open Picture'
      TabOrder = 0
      OnClick = btnOpenClick
  end
  object btnGrayBitmap: TBitBtn
    Left = 8
      Top = 40
      Width = 75
      Height = 25
      Caption = '&Gray Bitmap'
      TabOrder = 1
      OnClick = btnGrayBitmapClick
  end
  object btnGrayGraphic: TBitBtn
    Left = 8
      Top = 72
      Width = 75
      Height = 25
      Caption = '&Gray Graphic'
      TabOrder = 2
      OnClick = btnGrayGraphicClick
  end
  object OpenPic: TOpenPictureDialog
    Left = 32
      Top = 136
  end
end

The form has three buttons. The first will load a picture and show it in the Image control. The second will try to gray the graphic stored in the picture property of the TImage as if it was a Bitmap (it will fail if it isn't a Bitmap). And the third will call the GrayGraphic which will work for bitmaps and other compatible TGraphic descendants.

Try to load Jpegs to see taht the code work even with other TGraphics. If you have other third-party supplied, and fully working TGraphic descendant, try adding them to the unit1 (TGifImage for example), and you'll see that it also works with them.

I hope that you can get some good things out of this article (ScanLine manipulation, bitmap format information, TGraphic relationships, etc.) or that it proves useful to you.

Nincsenek megjegyzések:

Megjegyzés küldése