Bitmaps vergroten met interpolatie

download program
download Delphi (7) project

Inleiding

De Delphi TBitmap class bevat een twee dimensionaal array van bytes,words of cardinals
genaamd canvas dat een afbeelding kan weergeven.
Een individueel byte,word,cardinal van dit canvas heet pixel.

Soms moet een afbeelding worden vergroot, bijvoorbeeld om gelijke grootte te handhaven
bij hogere resoluties zoals bij een printer canvas.
Dat kan geschieden door de bitmap te kopiëren naar een andere van verschillende grootte,
gebruik makend van een geschikt algoritme.

Een manier is om de pixels van de nieuwe bitmap één voor één te projecteren
op de pixels van de originele bitmap en de gemiddelde waarde van dat gebied te berekenen.
Ik noem dat de "projectie methode".

Kijk hier voor een beschrijving van deze methode.

Dat werkt prima in het geval van verkleining.
Maar bij vergroting met een factor 2 of 3 is het resultaat niet vloeiend.
In dit artikel beschrijf ik een andere methode die geschikter is:
    - kopieer de bronpixels direct naar de plek van hun bestemming
    - gebruik interpolatie om de lege pixels van de bestemmings bitmap op te vullen
Ik noem dat de "interpolatie methode".
De volgende plaatjes laten de resultaten zien bij 3 voudige vergroting:

origineel projectie methode interpolatie methode

Met een vergrootglas is het verschil nog duidelijker te zien.

TBitmap class

In dit Delphi-7 project zijn de pixels 32 bit positieve integers (pf32bit).

Dit is het interne format van zo'n pixel:



Per kleur zijn er 8 bits voor de intensiteit, die dus kan varieren van 0..255.
Bits 24..31 worden niet gebruikt.

Het volgende plaatje laat de coördinaten van de pixels op een canvas zien.
De bitmap scanline[y] property verschaft een pointer naar het eerste pixel van rij y.



Een pixel kan gelezen of geschreven worden met .canvas.pixels[x,y]
Dit is een relatief traag proces dat alleen geschikt is als het om weinig pixels gaat.
Veel (50x) sneller is het adresseren van pixels met pointers naar hun geheugenplaats.
Voor makkelijke berekening met deze pointers sla ik ze op als dwords (of cardinal).
Hierna maken we een bitmap genaamd map met 100 rijen en 200 kolomman:
type PDW = ^dword;
...
var map : TBitmap;
    p0  : dword;//pointer to [0,0]
    pstep : dword;//pointer distance between rows
....
begin
 map := TBitmap.create;
 with map do
  begin
   width := 200;
   height := 100;
   pixelformat := pf32bit;
  end;	
  p0 := dword(map.scanline[0]);
  pstep := p0 - dword(map.scanline[1]);
Nu kan de opdracht code
 color1 := map.canvas.pixels[12,75) //----1
vervangen worden door
  color1 := PWD(p0 - 75*pstep + (12 shl 2))^; //------2
Opmerking:
In gevallen -1- en -2- hiervoor zijn de waarden van variabele color1 iets verschillend.
In geval -1- staat het rode veld in bits 0..7 en blauw in bits 16..23.
Dit is het Windows format voor 32 en 24 bit kleuren.

Bij beschouwing van een 100*200 pixels canvas als één dimensinaal array[0..19999] van dwords,
is het eerste dword [0] pixel positie [0,199], de links onder hoek.
Dword [1] ligt op plaats [1,199] dat is 4 bytes verderop.
Gaande van [0,199] naar [0,198] vraagt optelling van 4*200 = 800 bij de pointer.
Pointers zijn byte adressen.
De expressie (12 shl 2) is een snelle manier om 12 met 4 te vermenigvuldigen.
De volgende rij in een kolom bereiken we door van de pointer 4 keer de waarde "aantal kolommen" af te trekken.

Opmerking:
Een bitmap is ook als een ééndimensionaal array A[ ] te beschouwen,
waarbij bitmap.scanline[bitmap.height-1] naar A[0] wijst.

Vergroten x 2

Een 5*5 bitmap vergroten we naar 10*10.


De pixels die direct overgezet worden naar de nieuwe bitmap zijn met een getal aangegeven.
Daarna worden de tussenliggende pixels A,B,C have berekend.
De onderste rij en ook de meest rechtse kolom hebben een aparte aanpak nodig.

Het project
Voor details verwijs ik naar de source code.
Er zijn twee units, -1- voor controle en afhandeling van events.
De expansion_unit bevat de procedures voor de bitmap vergroting.

Het begint met laden van een afbeelding in bitmap map1.
.bmp en .jpg formaten zijn mogelijk.
map1 wordt afgebeeld in paintbox1.
bitmap map2 wordt gemaakt met 2x de breedte en hoogte van map1.
procedure X2copy(map2,map1) produceert daarna de vergroting.

procedure X2copy
type TAIP = arry[1..8] of dword;
var c1,c2,c3,c4 : dword; //colors of source map
    AIP : TAIP;          //interpolation pixels
    pd0 : dword;         //destination row 0 pointer  
    pd1,pd2 : dword;     //destination row pointers
    pdstep : dword;      //destination row difference
    ps0 : dword;         //source row 0 pointer
    psstep : dword;      //source row difference
    x,y : word;          //source pixel addressing 
    py,py1 : dword;      //scratch pointers


De pixels worden verwerkt van links boven naar rechts onder, zoals we een boek lezen.
variabelen x,y adresseren het bron pixel c1 {c staat voor color}.
C1 wordt naar map2 gekopieerd.
Pixels A,B,C worden berekend in
procedure interpolate24(AIP,c1,c2,c3,c4);

A is AIP[1]
B is AIP[2]
C is AIP[3]

Interpolate24...{2x vergroting, 4 variabelen} roept aan
procedure unpackColor(var r,g,b : byte; col : dword);
begin
 b := col and $ff;
 col := col shr 8;
 g := col and $ff;
 col := col shr 8;
 r := col and $ff;
end;
voor het extraheren van de 8 bit r,g,b kleuren uit dword col.

c1 heeft de r1,g1,b1 variabelen voor rood, groen, blauw.
c2 heeft de r2,g2,b2 variabelen...etc.

Dan worden de r,g,b waardes voor elk pixel A,B,C berekend.

A = (C1+C2)/2 for r,g,b
B = (C1+C3)/2 for r,g,b
C + (C1+C2+C3+C4+3)/4 for r,g,b

Tenslotte worden de r,g,b kleuren verpakt in AIP[1], AIP[2}..etc door aanroep van
procedure PackColor(var col : dword; r,g,b : byte);
begin
 col := ((r shl 16) or (g shl 8) or b);
end;
Zie de source code voor details.

Rechter kolom



Pixel c1 op [x,y] wordt direct gekopieerd.
Pixel A is gelij aan c1.
Pixels B en C zijn het gemiddelde van c1 en c2 wat berekend wordt door
procedure interpolate22(var AIP : TAIP; c1,c2 : dword);
//return AIP[1]
var r,g,b,r1,g1,b1,r2,g2,b2 : byte;
begin
 UnpackColor(r1,g1,b1,c1);
 UnpackColor(r2,g2,b2,c2);
 r := (r1 + r2) shr 1;
 g := (g1 + g2) shr 1;
 b := (b1 + b2) shr 1;
 packcolor(AIP[1],r,g,b);
end; 
Onderste rij



c1 wordt gekopieerd.
B is gelijk aan c1.
A en C zijn het gemiddelde van c1,c2.

Vergroten met factor 3

Hieronder staat een 3x4 bitmap en de 3 voudige vergroting.



De aanpak is hetzelfde als bij vergroting met factor 2 maar er is meer rekenwerk vereist.
Eerst worden de 3x3 velden aangepakt.

De interpolatie is wat lastiger.
Ik bereken de pixels A,B,C,D,E,F,G,H als gewogen gemiddeldes van pixels C1,C2,C3,C4.
Als een pixel (C1,C2..) afstand d heeft tot pixel (A,B,C...) dan is zijn weegfactor w = 1/d.
De afstanden worden met de stelling van Pythagoras berekend.

A = (1.C1 + 0.5C2)/(1+0.5) = 0.66C1 + 0.33C2
B = (0.5C2 + 1.C1)/(1+0.5) = 0.33C1 + 0.66C2
D = 0.36C1 + 0.23C2 + 0.23C3 + 0.18C4
zie het volgende plaatje:



De berekening moet uiteraard voor elke kleur (r,g,b) van D worden gemaakt.

Bovenste rij en linker kolom
Deze pixels zijn een kopie van hun (onderliggende, rechtse) buren.

Rechter kolom



C1,C2 en interpolatie kleuren A,B worden worden ook naar de rechter kolom gekopieerd.

Onderste rij



C1 en C2 worden rechtstreeks gekopieerd.
A,B zijn de interpolatie pixels.
C1,C2,A,B worden gekopieerd naar de onderste rij van de niewe bitmap.

Resultaten zichtbaar maken

map1 (geladen van disk) wordt zichtbaar door kopiëren naar paintbox1.
Paintbox1 heeft een vaste breedte en hoogte van 400*400 pixels.

map2 is het resultaat van de vergroting en wordt getoond in paintbox2.
Deze paintbox meet 800*800 pixels.
Een horizontale- en vertikale scrollbar maakt mogelijk om grotere afbeeldingen te bekijken.

De scrollbar max property moet per afbeelding worden aangepast.
Dat doet de volgende code
var d : smallInt;
...
begin
 d := map2.width - paintbox2.Width;
 if d < 0 then d := 0;
 Hscrollbar.max := d;
 Hscrollbar.position := 0;
 d := map2.Height - paintbox2.Height;
 if d < 0 then d := 0;
 Vscrollbar.Max := d;
 Vscrollbar.position := 0;
end; 
Een scrollbar onChange event zorgt voor berekening van de rectangle op map2 die naar paintbox2 gekopieerd word:
procedure TForm1.VscrollbarChange(Sender: TObject);
//V,H scrollbar changes
//repaint paintbox2
var BW,BH : word;//paintbox width,height
    rs,rd : Trect; //source,destination rect
begin
 BW := paintbox2.Width;
 BH := paintbox2.Height;
 with rs do
  begin
   left := Hscrollbar.position;
   top := Vscrollbar.position;
   right := left + BW;
   bottom := top + BH;
  end;
 with rd do
  begin
   left := 0;
   top := 0;
   right := BW;
   bottom := BH;
  end;
 paintbox2.Canvas.CopyRect(rd,map2.Canvas,rs);
end;


Als afsluiting toon ik een voorbeeld van drievoudige vergroting met zowel de
projectie- als de interpolatie methode:

origineel:


projectie methode:


interpolatie methode:


Afgebeeld is het wapen van de provincie Zeeland.
Een wat vrije vertaling van de tekst (Luctor Et Emergo) is : "pompen of verzuipen".