1
{*****************************************************************************}
3
This file is part of the Free Pascal's "Free Components Library".
4
Copyright (c) 2005 by Giulio Bernardi
6
This file contains classes used to quantize images.
8
See the file COPYING.FPC, included in this distribution,
9
for details about the copyright.
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15
{*****************************************************************************}
22
uses sysutils, classes, fpimage, fpcolhash;
25
FPQuantizerException = class (exception);
28
TFPQuantizerProgressEvent = procedure (Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte;
29
const Msg: AnsiString; var Continue : Boolean) of object;
32
TFPColorQuantizer = class
34
FOnProgress : TFPQuantizerProgressEvent;
37
FSupportsAlpha : boolean;
38
FImages : array of TFPCustomImage;
40
function InternalQuantize : TFPPalette; virtual; abstract;
41
procedure SetColNum(AColNum : longword); virtual;
42
procedure Progress (Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte; const Msg: AnsiString; var Continue : Boolean); virtual;
43
function GetImage(Index : integer) : TFPCustomImage;
44
procedure SetImage(Index : integer; const Img : TFPCustomImage);
45
procedure SetCount(Value : integer);
47
property OnProgress : TFPQuantizerProgressEvent read FOnProgress write FOnProgress;
48
property Images[Index : integer] : TFPCustomImage read GetImage write SetImage;
49
property Count : integer read FCount write SetCount;
50
property ColorNumber : longword read FColNum write SetColNum;
51
property SupportsAlpha : boolean read FSupportsAlpha;
53
procedure Add(const Img : TFPCustomImage);
54
function Quantize : TFPPalette;
55
constructor Create; virtual;
56
destructor Destroy; override;
61
POctreeQNode = ^TOctreeQNode;
62
TOctreeQChilds = array[0..7] of POctreeQNode;
67
Next : POctreeQNode; //used in the reduction list.
68
Childs : TOctreeQChilds;
73
TFPOctreeQuantizer = class(TFPColorQuantizer)
76
ReductionList : TOctreeQChilds;
77
LeafTot, MaxLeaf : longword;
78
percent : byte; { these values are used to call OnProgress event }
79
percentinterval : longword;
80
percentacc : longword;
82
procedure DisposeNode(var Node : POctreeQNode);
83
procedure AddColor(var Node : POctreeQNode; const R, G, B, Level : byte);
84
procedure AddToPalette(var Node : POctreeQNode; Palette : TFPPalette; var Current : integer);
86
function BuildPalette : TFPPalette;
88
function InternalQuantize : TFPPalette; override;
94
total, startindex, endindex : longword;
102
TFPMedianCutQuantizer = class(TFPColorQuantizer)
104
HashTable, palcache : TFPColorHashTable;
105
arr : TFPColorWeightArray;
106
boxes : array of TMCBox;
108
percent : byte; { these values are used to call OnProgress event }
109
percentinterval : longword;
110
percentacc : longword;
113
function ColorCompare(const c1, c2 : TFPPackedColor; const Dim : byte) : shortint;
114
function FindLargestDimension(const Box : TMCBox) : byte;
115
procedure QuickSort(const l, r : integer; const Dim : byte);
116
procedure QuickSortBoxes(const l, r : integer);
117
function MeanBox(const box : TMCBox) : TFPColor;
118
function BuildPalette : TFPPalette;
119
procedure SetMode(const Amode : byte);
120
function MaskColor(const col : TFPColor) : TFPColor;
122
function InternalQuantize : TFPPalette; override;
124
constructor Create; override;
125
property Mode : byte read FMode write SetMode;
130
function RGB2FPColor(const R, G, B : longword) : TFPColor;
132
Result.Red:=(R shl 8) + R;
133
Result.Green:=(G shl 8) + G;
134
Result.Blue:=(B shl 8) + B;
135
Result.Alpha := AlphaOpaque;
138
{ TFPColorQuantizer }
140
function TFPColorQuantizer.Quantize : TFPPalette;
142
Result:=InternalQuantize;
145
constructor TFPColorQuantizer.Create;
147
FSupportsAlpha:=false;
148
FColNum:=256; //default setting.
150
setlength(FImages,0);
153
destructor TFPColorQuantizer.Destroy;
155
Setlength(FImages,0);
159
procedure TFPColorQuantizer.SetColNum(AColNum : longword);
162
raise FPQuantizerException.Create('Invalid color depth: '+IntToStr(AColNum));
166
procedure TFPColorQuantizer.Progress(Sender: TObject; Stage: TFPImgProgressStage; PercentDone: Byte; const Msg: AnsiString; var Continue : Boolean);
168
if Assigned(FOnProgress) then
169
FOnProgress(Sender,Stage,PercentDone,Msg,Continue);
172
function TFPColorQuantizer.GetImage(Index : integer) : TFPCustomImage;
174
if Index>=FCount then
175
raise FPQuantizerException.Create('Invalid image index: '+IntToStr(Index));
176
Result:=FImages[index];
179
procedure TFPColorQuantizer.SetImage(Index : integer; const Img : TFPCustomImage);
181
if Index>=FCount then
182
raise FPQuantizerException.Create('Invalid image index: '+IntToStr(Index));
186
procedure TFPColorQuantizer.SetCount(Value : integer);
187
var old, i : integer;
190
setlength(FImages,Value);
191
for i:=old to Value-1 do
196
procedure TFPColorQuantizer.Clear;
198
setlength(FImages,0);
202
procedure TFPColorQuantizer.Add(const Img : TFPCustomImage);
205
{ Find first unused slot }
206
for i:=0 to FCount-1 do
207
if FImages[i]=nil then
212
{ If we reached this point there are no unused slot: let's enlarge the array }
214
FImages[FCount-1]:=Img;
217
{ TFPOctreeQuantizer }
219
const Mask : array[0..7] of byte = ($80, $40, $20, $10, $08, $04, $02, $01);
221
procedure TFPOctreeQuantizer.AddColor(var Node : POctreeQNode; const R, G, B, Level : byte);
222
var index, shift : byte;
226
Node:=getmem(sizeof(TOctreeQNode));
228
raise FPQuantizerException.Create('Out of memory');
229
FillByte(Node^,sizeof(TOctreeQNode),0);
233
inc(LeafTot); { we just created a new leaf }
236
begin { we don't put leaves in reduction list since this is unuseful }
238
Node^.Next:=ReductionList[level]; { added on top of the reduction list for its level }
239
ReductionList[level]:=Node;
252
index:=((R and mask[level]) shr shift) shl 2;
253
index:=index+((G and mask[level]) shr shift) shl 1;
254
index:=index+((B and mask[level]) shr shift);
255
AddColor(Node^.Childs[index],R,G,B,Level+1);
259
procedure TFPOctreeQuantizer.DisposeNode(var Node : POctreeQNode);
262
if Node=nil then exit;
263
if not (Node^.isleaf) then
265
if Node^.childs[i]<>nil then
266
DisposeNode(Node^.childs[i]);
271
procedure TFPOctreeQuantizer.Reduce;
275
i:=6; { level 7 nodes don't have childs, start from 6 and go backward }
276
while ((i>0) and (ReductionList[i]=nil)) do
279
{ remove this node from the list}
280
Node:=ReductionList[i];
281
ReductionList[i]:=Node^.Next;
284
if Node^.childs[i]<>nil then
286
inc(Node^.count,Node^.childs[i]^.count);
287
inc(Node^.r,Node^.childs[i]^.r);
288
inc(Node^.g,Node^.childs[i]^.g);
289
inc(Node^.b,Node^.childs[i]^.b);
290
DisposeNode(Node^.childs[i]);
294
inc(LeafTot); { this node is now a leaf! }
297
procedure TFPOctreeQuantizer.AddToPalette(var Node : POctreeQNode; Palette : TFPPalette; var Current : integer);
300
if not FContinue then exit;
304
if (current >= LeafTot) then
305
raise FPQuantizerException.Create('Octree Quantizer internal error: palette index too high.');
306
Node^.r:= Node^.r div Node^.count;
307
Node^.g:= Node^.g div Node^.count;
308
Node^.b:= Node^.b div Node^.count;
309
Palette.Color[Current]:=RGB2FPColor(Node^.r,Node^.g,Node^.b);
312
{ ************************************************ }
314
if percentacc>=percentinterval then
316
dec(percentacc,percentinterval);
318
Progress(self,psRunning,percent,'',FContinue);
320
{ ************************************************ }
325
if Node^.childs[i]<>nil then
326
AddToPalette(Node^.childs[i],Palette,Current);
329
function TFPOctreeQuantizer.BuildPalette : TFPPalette;
330
var pal : TFPPalette;
333
if Root=nil then exit;
334
pal:=TFPPalette.Create(LeafTot);
337
AddToPalette(Root,pal,i);
343
if not FContinue then
351
function TFPOctreeQuantizer.InternalQuantize : TFPPalette;
352
var i, j, k : integer;
356
for i:=0 to high(ReductionList) do
357
ReductionList[i]:=nil;
361
{ ************************************************************** }
362
{ set up some values useful when calling OnProgress event }
363
{ number of operations is: }
364
{ width*heigth for population }
365
{ initial palette count - final palette count for reduction }
366
{ final palette count for building the palette }
367
{ total: width*heigth+initial palette count. }
368
{ if source image doesn't have a palette assume palette count as }
369
{ width*height (worst scenario) if it is < 2^24, or 2^24 else }
372
for i:=0 to FCount-1 do
373
if FImages[i]<>nil then
375
percentinterval:=percentinterval+FImages[i].Width*FImages[i].Height;
376
if FImages[i].UsePalette then
377
percentacc:=percentacc+FImages[i].Palette.Count
379
percentacc:=percentacc+FImages[i].Width*FImages[i].Height;
381
if percentacc>$1000000 then percentacc:=$1000000;
383
percentinterval:=(percentacc+percentinterval) div 100; { how many operations for 1% }
384
if percentinterval=0 then percentinterval:=$FFFFFFFF; { it's quick, call progress only when starting and ending }
388
Progress (self,psStarting,0,'',FContinue);
390
if not FContinue then exit;
391
{ ************************************************************** }
393
{ populate the octree with colors }
395
for k:=0 to FCount-1 do
396
if FImages[k]<>nil then
397
for j:=0 to FImages[k].Height-1 do
398
for i:=0 to FImages[k].Width-1 do
400
Color:=FImages[k][i,j];
401
AddColor(Root,(Color.Red and $FF00) shr 8,(Color.Green and $FF00) shr 8,(Color.Blue and $FF00) shr 8,0);
402
{ ************************************************* }
404
if percentacc>=percentinterval then
406
dec(percentacc,percentinterval);
408
Progress(self,psRunning,percent,'',FContinue);
409
if not FContinue then exit;
411
{ ************************************************* }
413
{ reduce number of colors until it is <= MaxLeaf }
414
while LeafTot > MaxLeaf do
417
{ ************************************************* }
419
if percentacc>=percentinterval then
421
dec(percentacc,percentinterval);
423
Progress(self,psRunning,percent,'',FContinue);
424
if not FContinue then exit;
426
{ ************************************************* }
429
{ build the palette }
430
Result:=BuildPalette;
431
if FContinue then Progress (self,psEnding,100,'',FContinue);
437
{ TFPMedianCutQuantizer }
444
constructor TFPMedianCutQuantizer.Create;
447
FSupportsAlpha:=true;
451
procedure TFPMedianCutQuantizer.SetMode(const Amode : byte);
453
if not (Amode in [mcSlow,mcNormal,mcFast]) then
454
raise FPQuantizerException.Create('Invalid quantizer mode: '+IntToStr(Amode));
458
function TFPMedianCutQuantizer.FindLargestDimension(const Box : TMCBox) : byte;
460
col : TFPPackedColor;
461
maxa, mina, maxr, minr, maxg, ming, maxb, minb : byte;
463
maxa:=0; maxr:=0; maxg:=0; maxb:=0;
464
mina:=$FF; minr:=$FF; ming:=$FF; minb:=$FF;
465
for i:=box.startindex to box.endindex do
468
if col.A<mina then mina:=col.A;
469
if col.A>maxa then maxa:=col.A;
470
if col.R<minr then minr:=col.R;
471
if col.R>maxr then maxr:=col.R;
472
if col.G<ming then ming:=col.G;
473
if col.G>maxg then maxg:=col.G;
474
if col.B<minb then minb:=col.B;
475
if col.B>maxb then maxb:=col.B;
481
if ((maxa>maxr) and (maxa>maxg) and (maxa>maxb)) then Result:=DIM_ALPHA
482
else if ((maxr>maxa) and (maxr>maxg) and (maxr>maxb)) then Result:=DIM_RED
483
else if ((maxg>maxa) and (maxg>maxr) and (maxg>maxb)) then Result:=DIM_GREEN
484
else Result:=DIM_BLUE;
487
function TFPMedianCutQuantizer.ColorCompare(const c1, c2 : TFPPackedColor; const Dim : byte) : shortint;
491
DIM_ALPHA : tmp:=(c1.A-c2.A);
492
DIM_RED : tmp:=(c1.R-c2.R);
493
DIM_GREEN : tmp:=(c1.G-c2.G);
494
DIM_BLUE : tmp:=(c1.B-c2.B)
495
else raise FPQuantizerException.Create('Invalid dimension: '+IntToStr(Dim));
497
if tmp>0 then Result:=1
498
else if tmp<0 then Result:=-1
502
procedure TFPMedianCutQuantizer.QuickSort(const l, r : integer; const Dim : byte);
504
pivot, temp : PFPColorWeight;
512
while ((i<=r) and (ColorCompare(arr[i]^.Col,pivot^.Col,dim)<=0)) do
514
while (ColorCompare(arr[j]^.Col,pivot^.Col,dim)=1) do
523
{ don't swap if they are equal }
524
if ColorCompare(arr[j]^.Col,pivot^.Col,dim)<>0 then
529
Quicksort(l,j-1,dim);
534
procedure TFPMedianCutQuantizer.QuickSortBoxes(const l, r : integer);
536
pivot, temp : TMCBox;
544
while ((i<=r) and (boxes[i].total>=pivot.total)) do
546
while (boxes[j].total<pivot.total) do
555
{ don't swap if they are equal }
556
if boxes[j].total<>pivot.total then
561
QuicksortBoxes(l,j-1);
566
function TFPMedianCutQuantizer.MeanBox(const box : TMCBox) : TFPColor;
567
var tota,totr,totg,totb, pixcount : longword;
569
col : TFPPackedColor;
572
tota:=0; totr:=0; totg:=0; totb:=0; pixcount:=0;
573
for i:=box.startindex to box.endindex do
575
tota:=tota+(arr[i]^.Col.A*arr[i]^.Num);
576
totr:=totr+(arr[i]^.Col.R*arr[i]^.Num);
577
totg:=totg+(arr[i]^.Col.G*arr[i]^.Num);
578
totb:=totb+(arr[i]^.Col.B*arr[i]^.Num);
579
inc(pixcount,arr[i]^.Num);
581
tota:=round(tota / pixcount);
582
totr:=round(totr / pixcount);
583
totg:=round(totg / pixcount);
584
totb:=round(totb / pixcount);
585
if tota>$FF then tota:=$FF;
586
if totr>$FF then totr:=$FF;
587
if totg>$FF then totg:=$FF;
588
if totb>$FF then totb:=$FF;
593
fpcol:=Packed2FPColor(col);
594
if palcache.Get(fpcol)<>nil then { already found, try the middle color }
596
fpcol:=Packed2FPColor(arr[(box.startindex+box.endindex) div 2]^.Col);
597
if palcache.Get(fpcol)<>nil then { already found, try the first unused color }
598
for i:=box.startindex to box.endindex do
600
col.a:=arr[i]^.Col.A;
601
col.r:=arr[i]^.Col.R;
602
col.g:=arr[i]^.Col.G;
603
col.b:=arr[i]^.Col.B;
604
fpcol:=Packed2FPColor(col);
605
if palcache.Get(fpcol)=nil then break;
608
palcache.Insert(fpcol,nil);
612
function TFPMedianCutQuantizer.BuildPalette : TFPPalette;
613
var pal : TFPPalette;
616
pal:=TFPPalette.Create(Used);
618
palcache:=TFPColorHashTable.Create;
620
for i:=0 to Used-1 do
622
pal.Color[i]:=MeanBox(boxes[i]);
623
{ ************************************************* }
625
if percentacc>=percentinterval then
627
percentacc:=percentacc mod percentinterval;
629
Progress(self,psRunning,percent,'',FContinue);
630
if not FContinue then exit;
632
{ ************************************************* }
644
{ slow mode: no filtering
645
normal mode: 8 bit r, 6 bit g, 6 bit b
646
fast mode: 5 bit r, 5 bit g, 5 bit b }
648
const mask_r_normal = $FFFF;
649
mask_g_normal = $FCFC;
650
mask_b_normal = $FCFC;
655
function TFPMedianCutQuantizer.MaskColor(const col : TFPColor) : TFPColor;
660
Result.Red:=Col.Red and mask_r_normal;
661
Result.Green:=Col.Green and mask_g_normal;
662
Result.Blue:=Col.Blue and mask_b_normal;
666
Result.Red:=Col.Red and mask_r_fast;
667
Result.Green:=Col.Green and mask_g_fast;
668
Result.Blue:=Col.Blue and mask_b_fast;
674
function TFPMedianCutQuantizer.InternalQuantize : TFPPalette;
678
boxpercent : longword;
680
HashTable:=TFPColorHashTable.Create;
682
{ *****************************************************************************
684
width*height of each image (populate the hash table)
685
number of desired colors for the box creation process (this should weight as the previous step)
686
number of desired colors for building the palette.
689
for k:=0 to FCount-1 do
690
if FImages[k]<>nil then
691
percentinterval:=percentinterval+FImages[k].Height*FImages[k].Width;
692
boxpercent:=percentinterval div FColNum;
693
percentinterval:=percentinterval*2+FColNum;
695
percentinterval:=percentinterval div 100; { how many operations for 1% }
696
if percentinterval=0 then percentinterval:=$FFFFFFFF; { it's quick, call progress only when starting and ending }
700
Progress (self,psStarting,0,'',FContinue);
701
if not FContinue then exit;
702
{ ***************************************************************************** }
704
{ For every color in the images, count how many pixels use it}
705
for k:=0 to FCount-1 do
706
if FImages[k]<>nil then
707
for j:=0 to FImages[k].Height-1 do
708
for i:=0 to FImages[k].Width-1 do
710
HashTable.Add(MaskColor(FImages[k][i,j]),1);
711
{ ************************************************* }
713
if percentacc>=percentinterval then
715
percentacc:=percentacc mod percentinterval;
717
Progress(self,psRunning,percent,'',FContinue);
718
if not FContinue then exit;
720
{ ************************************************* }
722
{ Then let's have the list in array form }
724
arr:=HashTable.GetArray;
726
HashTable.Clear; { free some resources }
728
setlength(boxes,FColNum);
729
boxes[0].startindex:=0;
730
boxes[0].endindex:=length(arr)-1;
731
boxes[0].total:=boxes[0].endindex+1;
734
while (used<FColNum) do
737
{ find a box with at least 2 colors }
738
for i:=0 to Used-1 do
739
if (boxes[i].total)>=2 then
744
if box=nil then break;
746
dim:=FindLargestDimension(box^);
747
{ sort the colors of the box along the largest dimension }
748
QuickSort(box^.startindex,box^.endindex,dim);
750
{ Split the box: half of the colors in the first one, the rest in the second one }
751
j:=(box^.startindex+box^.endindex) div 2;
752
{ This is the second box }
753
boxes[Used].startindex:=j+1;
754
boxes[Used].endindex:=box^.endindex;
755
boxes[Used].total:=box^.endindex-j;
756
{ And here we update the first box }
758
box^.total:=box^.endindex-box^.startindex+1;
759
{ Sort the boxes so that the first one is the one with higher number of colors }
760
QuickSortBoxes(0,Used);
763
{ ************************************************* }
764
inc(percentacc,boxpercent);
765
if percentacc>=percentinterval then
767
inc(percent,percentacc div percentinterval);
768
percentacc:=percentacc mod percentinterval;
769
Progress(self,psRunning,percent,'',FContinue);
770
if not FContinue then exit;
772
{ ************************************************* }
774
Result:=BuildPalette;
775
if FContinue then Progress (self,psEnding,100,'',FContinue);
778
for i:=0 to length(arr)-1 do
b'\\ No newline at end of file'