Pic-of-Pictures - Bilder bilden Bilder (Foto-Mosaik)
Photo-Mosaic-Tutorial von Daniel Schwamm (09.02.2008)
Inhalt
An dem Proggy ist mein Kollege Schuld. Er erwähnte, er habe in einem
Werbeprospekt ein cooles Bild gesehen, das aus lauter Einzelbildern
zusammengebaut war, die so geschickt arrangiert wurden, das sich daraus
ein neues grosses Bild ergab. Also eine Collage aus Fotos, eine Wallpaper
aus JPGs.
Das hörte sich witzig an! Eine nette Herausforderung für den Schwamm!
Und so hatte er seine Beschreibung noch nicht ganz beendet, da hatte mein
verkorkstes Hirn schon eine wage Idee ausgebrütet, wie man so etwas würde
selbst programmieren können. Einen Mosaiken-Generator, der die Einzelbilder
vollautomatisch zu einer Collage montiert, statt dies mit Hilfe einer
Bildbearbeitung wie Photoshop mühsam selbst von Hand realisieren zu müssen.
Gleicher Tag, Abends zu Hause. In der Glotze kam mal wieder nichts. Also warf
ich den PC an und hatte irgendwann im Morgengrauen meine erste Version von
PicOfPics fertig.
Das endgültige Ergebnis liess deutlich länger auf sich warten. Klar.
Zumal mir ohne Ende weitere Schikanen einfielen, wie man das Programm
komplexer und optionsreicher gestalten könnten. Heraus kam letztlich
ein nettes kleines Tool, das allemal gut genug ist, um ein paar
Aha-Effekte beim staunenden Publikum hervorrufen zu können.
Die prinzipielle Vorgehensweise von Pic-of-Pics ist folgende:
-
Zuerst wird der Quader-Pool definiert. Hier werden die Bilder verwaltet,
aus denen sich später das Ergebnisbild zusammen setzt. Also quasi die Thumbnails für
das spätere Gesamtbild. Diese Bilder nenne ich Quader.
Man wählt hierzu einen Ordner, scannt die darin befindlichen JPG-Bilder, bestimmt
ihre "Mittel-Farbe" und die 3 x 3 "Verlaufsfarben", und merkt sich die Ergebnisse in
einer TStringGrid.
Ein Histogramm zeigt an, wie gut die Mittel-Farben der Quader-Bilder die 255 möglichen
Graustufen von Weiss bis Schwarz abdecken.
Die Grösse der Quader ist ohne Belang. Optimal sind quadratische Bilder.
Alle Funktionen werden in der Delphi-Unit "qp_u.pas" gekapselt. Sie beginnen
mit dem Prefix "qp_".
-
Dann lädt man das Originalbild ein, welches "nachgebaut" werden soll.
Auch hier zeigt ein Histogramm an, welche Graustufen, also welche Helligkeitsgrade,
im Bild vorkommen. Idealerweise decken sich natürlich die Histogramme von Quader-Pool
und Originalbild einigermassen.
Der Source zum Originalbild wird in der Unit "ob_u.pas" untergebracht. Die
Funktionen beginnen entsprechend mit "ob_".
-
Jetzt kann man in der Optionen-Page Einstellungen vornehmen, die die
fertige Bildersammlung beeinflussen.
So kann die Anzahl der horizontalen Quader-Bilder
angegeben werden, die im Ergebnisbild Verwendung finden sollen (die vertikale
Anzahl ergibt sich automatisch, da stets mit quadratischen Quadern gearbeitet wird).
Des Weiteren kann die Breite der Quader in Pixeln angegeben werden, ob ihre Position
"verwackelt" oder exakt berechnet platziert werden sollen, ob sie einen Rand erhalten,
usw.
Fast alle Einstellungen lassen sich unmittelbar in der Vorschau betrachten und
so im Voraus für das Ergebnisbild abschätzen. Per Mausklick wird letztlich die
eigentlich Pic-of-Pics-Prozedur gestartet.
Das Originalbild wird dabei in eine "Pixel-Bitmap" umgewandelt, die so dimensioniert
ist, wie es durch die Anzahl der Quader vorgegeben wurde. Das Programm betrachtet nun
jeden Pixel der Pixel-Bitmap und sucht im Quader-Pool nach einem Bild, dessen Mittel-Farbe
in etwa dem des Pixels entspricht. Der gefundene Quader wird dann an passender Stelle und
mit der gewünschten Grösse in das Ergebnisbild kopiert.
Die Unit "op_u.pas" umfasst alle - mit "op_" beginnenden - Optionen-Funktionen.
-
Ist das Ergebnisbild fertig, kann - zur nachträglichen Verbesserung - das
Originalbild auf verschiedenen Arten in die Fotocollage eingeblendet werden.
Zudem kann in das Ergebnisbild rein- oder rausgezoomt werden, was eine eingehende
Betrachtung erlaubt. Und nicht zuletzt kann das Ergebnisbild natürlich auch gespeichert
werden.
Die "eb_"-Funktionen befinden sich in der Unit "eb_u.pas".
Wie bereits angedeutet, werden die vier Arbeitsschritte von PicOfPics jeweils durch
ein eigenes "Modul" abgearbeitet, zusammengefasst in Units. Auf der Hauptform wird
jede dieser Units durch eine Page eines TPageControls repräsentiert. Der Anwender
blättert sich so quasi vom Anfang bis zum fertigen Ergebnis durch.
Registerseite des Quader-Pools: Hier kann im fertigen Programm ausgewählt werden, aus welchen Einzelbildern sich die Mosaik-Collage zusammensetzen soll.
Registerseite des Originalbildes: Laden, Anzeigen und Histogramm-Analyse desjenigen Bildes, welches durch die verschiedenen Mosaik-Bilder wiedergegeben werden soll.
Registerseite der Optionen: Die eigentliche Bildgenerierung des Foto-Mosaiks kann hier auf mannigfaltige Möglichkeiten modifiziert werden.
Registerseite des Ergebnisbildes: Nach der Bildgenerierung gemäss der Thumbs, des Basisbildes und der Bildoptionen lässt sich hier das Ergebnisbild finden. Es kann nun noch nachträglich manipuliert und letztlich abgespeichert werden.
Die Funktionalität der meisten Buttons der Hauptform werden an die zugehörigen
Modul-Units einfach durchgereicht, weswegen hier nicht näher drauf eingegangen
wird.
Funktionen, die direkt die Hauptform betreffen oder die von mehreren Modulen
benötigt werden, sind dagegen in der Unit "hauptu.pas" definiert worden.
Dazu gehören zum Beispiel FormCreate bzw. FormDestroy. Hier werden im Wesentlichen
nur diverse Bitmaps erzeugt bzw. zerstört, die in den Modulen benötigt werden.
Zudem werden die Programmparameter aus der INI-Datei "picofpics.ini" geladen
bzw. gespeichert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
const
_caption='PicOfPics V1.0 (http://www.daniel-schwamm.de)';
_inifn='picofpics.ini';
_qpfn='qp.txt';
_cr=#13#10;
//counter-check fuer optimation
_cc_ok=true;
type
Thauptf = class(TForm)
[...]
public
{ Public-Deklarationen }
homedir:string;
//quad-pool------------------------
qp_histobmp:tbitmap;
qp_histoa:array[0..255]of double;
//original---------------------------
ob_bmp:tbitmap;
ob_pixelbmp:tbitmap;
ob_histobmp:tbitmap;
ob_histoa:array[0..255]of double;
//options--------------------------
op_prevbmp:tbitmap;
//result-----------------------------------
eb_bmp,eb_pbbmp,eb_blendbmp,eb_quadbmp,eb_orgbmp:tbitmap;
eb_hpos,eb_vpos:integer;
eb_scrollok:bool;
//counters for optimations
cc_setbuttonsc:integer;
cc_updateprevc:integer;
//functions
[...]
end;
//------------------------------------------------------
procedure Thauptf.FormCreate(Sender: TObject);
begin
caption:=_caption;
homedir:=extractfilepath(application.exename);
//counters for optimations
cc_setbuttonsc:=0;
cc_updateprevc:=0;
//main-pagecontrol
pctrl.align:=alclient;
pctrl.ActivePageIndex:=0;
//quad-pool-------------------------------
qp_histobmp:=tbitmap.create;
[...]
//read programm-parameters
with tinifile.create(homedir+_inifn) do begin
//window-position
top:=readinteger('param','top',top);
left:=readinteger('param','left',left);
width:=readinteger('param','width',width);
height:=readinteger('param','height',height);
if readbool('param','maximized',false) then
windowstate:=wsmaximized;
//quad-pool
qp_dlb.directory:=readstring('qp','qp_dlb','c:\');
qp_logchb.checked:=readbool('qp','qp_logchb',qp_logchb.checked);
[...]
free;
end;
qp_rdsg;
//visible true => onresize!
visible:=true;
qp_imgp.Height:=qp_flb.Height;
setbuttons;
end;
procedure Thauptf.FormDestroy(Sender: TObject);
begin
deletefile(homedir+_inifn);
with tinifile.create(homedir+_inifn) do begin
//window-position
if windowstate=wsmaximized then begin
writebool('param','maximized',true);
end
else begin
writeinteger('param','top',top);
writeinteger('param','left',left);
writeinteger('param','width',width);
writeinteger('param','height',height);
writebool('param','maximized',false);
end;
//quad-pool
writestring('qp','qp_dlb',qp_dlb.directory);
writebool('qp','qp_logchb',qp_logchb.checked);
[...]
free;
end;
//clean up
qp_histobmp.free;
[...]
end;
procedure Thauptf.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
canclose:=false;
if
not eb_u.eb_isempty and
eb_bmp.Modified
then begin
if application.MessageBox(
pchar(
'Das Ergebnis-Bild wurde modifiziert.'+_cr+
' Wirklich Pics2Pic ohne Speichern verlassen?'
),
'*** FRAGE ***',
mb_yesno
)=id_no then exit;
end;
canclose:=true;
end;
procedure Thauptf.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if key=vk_escape then close;
end;
procedure Thauptf.FormResize(Sender: TObject);
begin
eb_pbpaint(sender);
end;
Einige Funktionen finden in mehreren Modulen Verwendung. Sie werden als
"Service-Funktionen" ebenfalls in der Haupt-Unit implementiert.
Mit "getcoldiff" wird die Differenz zwischen zwei Farbwerten berechnet.
Da sich Farben bei Windows aus drei Farbkanälen zusammensetzen, nämlich rot,
grün und blau, ergibt sich die gesamte Farbdifferenz aus der Additionen der
absoluten Differenzen der Werte der drei Farbkanäle.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
//calculate the difference between two colors
function thauptf.getcoldiff(c1,c2:tcolor):integer;
begin
if(c1=-1)then begin
result:=-1;
exit;
end;
result:=
abs(getrvalue(c1)-getrvalue(c2))+
abs(getgvalue(c1)-getgvalue(c2))+
abs(getbvalue(c1)-getbvalue(c2));
end;
Um Farbwerte in Helligkeitswerte umzurechnen, werden diese in der Funktion
"col2helligkeit" in Grauwerte konvertiert, indem die Werte der drei Farbkanäle
aufaddiert und durch drei geteilt werden. Als Ergebnis erhält man einen Wert
zwischen 0 und 255 zurück, wobei 0 schwarz und 255 weiss entspricht.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
//calculate gray-value of a color
//=> 0=black, 255=white
function thauptf.col2helligkeit(col:tcolor):byte;
var
r,g,b:byte;
begin
r:=getrvalue(col);
g:=getgvalue(col);
b:=getbvalue(col);
result:=trunc((r+b+g)/3);
end;
Der direkte Zugriff auf die Pixel einer Canvas dauert in Delphi relativ lang.
Weitaus schneller ist die Methode, sich mittels der Scanline-Funktion von TBitmap
eine komplette Zeile einer Bitmap in ein PByteArray einzulesen, welches folgendermassen
gefüllt wird:
00001
00002
00003
00004
00005
00006
00007
00008
00009
blau grün rot blau grün rot blau grün rot ...
------------- ------------- -------------
Pixel 1 Pixel 2 Pixel 3
Beispiel:
0 0 0 255 255 255 0 0 0 0 0 255 ...
------- ----------- ------- -----------
schwarz weiss schwarz rot
Warum die Werte im PByteArray in der Reihenfolge "Blau-Grün-Rot" angeordnet sind,
statt wie man erwarten könnte als "Rot-Grün-Blau" hat mich Anfangs auch ziemlich
verwirrt. Das hat vermutlich was mit der internen Verarbeitung der Bitmaps zu
tun, wo die Farbwerte durch eine einzige grosse 32 Bit-Zahl repräsentiert werden
(nämlich als TColor), die sich wie folgt aufbaut:
00001
Farbwert = 255*255*Blauanteil + 255*Grünanteil + Rotanteil
Nun ja, um nun die PByteArray-Werte eines Pixels an Position "x" in einen Farbwert
umzurechnen, kann die Funktion "pba2col" verwendet werden.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
//convert a scanline-value at x to color
function thauptf.pba2col(
pba:pbytearray;
x:integer
):tcolor;
var
r,g,b:byte;
begin
r:=pba[x*3+2];
g:=pba[x*3+1];
b:=pba[x*3+0];
result:=rgb(r,g,b);
end;
In Pic-of-Pics baut sich das Ergebnisbild aus mehreren "Quader"-Bildern zusammen.
Diese Quader können später mittels der "verwackeln"-Prozedur zufällig abweichend
von ihrer idealen Positionen auf dem Ergebnisbild platziert werden, was einige
interessante Effekte erlaubt. Ebenso kann ihre Grösse variieren. Das Ausmass der
"Verwacklung" ergibt sich aus der "idealen" Breite des Quaders "v" und einem
optional veränderbaren Wert "proz".
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
//change quad-position and -size in random way
//(deoending on width of one quad)
function thauptf.verwackeln(v,proz:integer):integer;
var
i:integeR;
d:double;
begin
//verwacklung aktiv?
result:=0;if not op_wackelchb.Checked then exit;
d:=v/op_qbreitese.value;
i:=trunc(((op_qbreitese.value*proz)/100)/2);
result:=trunc((random(i)-random(i))*$);
end;
Die folgende Funktion "mkhistoimg" generiert aus einem Array "ca" eine
Histogramm-Grafik und kopiert diese nach "img". Das Array besteht aus
insgesamt 255 Einzelwerten. Jeder dieser Werte gibt wieder, wie oft der
zugehörige Index-Wert als Grauwert zuvor in einer Grafik ermittelt wurde.
Wenn z.B. "ca[0]" den Wert "30" enthält, so bedeutet das, dass die Grafik
exakt 30 Pixel mit dem Helligkeitswert 0 (also reines schwarz) enthält.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
//transfer gray-value-array to histogram-image
procedure thauptf.mkhistoimg(
img:timage;
ca:array of double
);
var
bmp:tbitmap;
c,h:integer;
max:double;
begin
bmp:=tbitmap.Create;
try
bmp.PixelFormat:=pf24bit;
//get maximum in gray-array
max:=0;
for c:=0 to 255 do begin
if ca[c]>max then max:=ca[c];
end;
if max=0 then max:=1;
//adapt histogramm dimension
bmp.Width:=256;
bmp.Height:=100+2+20;
//clean up bitmap
bmp.Canvas.Brush.color:=$0080FFFF;
bmp.Canvas.FillRect(rect(0,0,bmp.width,bmp.Height));
//paint histogramm
for c:=0 to 255 do begin
//change chart-color
if c mod 2=0 then bmp.canvas.pen.color:=clblue
else bmp.canvas.pen.color:=clnavy;
//norm to 100 percent
h:=trunc(ca[c]*100/max);
//set chart-hight to minimum 1 pixel
if(h=0)and(ca[c]>0)then h:=1;
//paint the chart
bmp.Canvas.moveto(c,bmp.Height-20-2-h);
bmp.Canvas.LineTo(c,bmp.Height-20-2);
bmp.canvas.pen.color:=rgb(c,c,c);
bmp.Canvas.moveto(c,bmp.Height-20);
bmp.Canvas.LineTo(c,bmp.Height);
end;
//save to histogram-image
img.Picture.Assign(bmp);
finally
bmp.Free;
end;
end;
Die letzten Service-Funktionen aktivieren bzw. deaktivieren die Buttons der
Hauptform je nach Programmstatus. So kann z.B. kein Ergebnisbild mittels des
"Speichern"-Knopfes gespeichert werden, so lange noch gar kein Ergebnisbild
vorliegt.
Des Weiteren werden die Hints von ScrollBars gesetzt, sodass sie den Wert der
aktuelle Position wieder geben. Und dann werden noch ein paar Counter gesetzt,
die anzeigen, wie oft bestimmte kritische Funktionen aufgerufen wurden, was bei
der Programm-Optimierung hilfreiche Informationen liefert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
//enabled/disable buttons on form
procedure thauptf.setbuttons;
begin
if not visible then exit;
inc(cc_setbuttonsc);countercheck;
//quad-pool---------------------------------
qp_neub.Enabled:=(qp_flb.Items.count>0);
if qp_neub.Enabled then qp_flb.color:=clwhite
else qp_flb.color:=clsilver;
qp_entfernenb.enabled:=not qp_isempty;
//original------------------------------
ob_entfernenb.enabled:=not ob_isempty;
[..]
end;
//set hints of scrollvars
procedure thauptf.setsbhint(sender:tobject);
var
sb:tscrollbar;
begin
if not(sender is tscrollbar) then exit;
sb:=tscrollbar(sender);
sb.hint:='Wert: '+inttostr(sb.position);
sb.showhint:=true;
end;
//for optimations
procedure thauptf.countercheck;
begin
if not _cc_ok then exit;
caption:=
_caption+' '+
'SetButtons: '+inttostr(cc_setbuttonsc)+' | '+
'UpdatePrev: '+inttostr(cc_updateprevc);
end;
Betrachten wir nun das Modul des "Quader-Pools". Hier werden die (kleinen)
Bilder (Thumbnails, "Quader") verwaltet, aus denen sich später das (grosse)
Ergebnisbild zusammensetzt.
Der Quader-Pool: Auf der linken Seite kann der Ordner mit den gewünschten Thumbnails ausgewählt werden. Oben muss dann auf 'Neu' geklickt werden. Die StringGrid wird daraufhin gefüllt. Die Durchschnittsfarbe und die 3 x 3 Verlaufsfarben wird wiedergegeben. Unten kann man sehen, welches Farbspektrum mit den Quader-Bildern abgedeckt wird.
Ganz oben haben wir die Buttons "Neu", eine ProgressBar und den Button "entfernen".
Ein Klick auf "Neu" bewirkt, dass der Quader-Pool neu gefüllt wird. Je nach Anzahl der
Quader kann dies einige Zeit benötigen, was in der ProgressBar angezeigt wird. Über
"Entfernen" kann der Quader-Pool jederzeit manuell gelöscht werden.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
//fill quad-grid with colors auf quad-pics
procedure qp_fillsg;
//get quality-value for quad-pic
//(smaller pics wil set to better quality)
function optscale(jpg:tjpegimage):tjpegscale;
var
w,h:integer;
begin
w:=jpg.width;
h:=jpg.height;
result:=jsEighth;
if (h<8)or(w<8) then
result:=jsfullsize
else if (h<50)or(w<50) then
result:=jshalf
else if (h<400)or(w<400) then
result:=jsquarter;
end;
//get and set smooth-colors in quad-grid
procedure setverlaufcolors(
bmp,pixelbmp:tbitmap;
r:integer
);
var
c,x,y:integer;
pba:pbytearray;
begin
//reduce quad-pic to 3 x 3 pixels
pixelbmp.canvas.StretchDraw(rect(0,0,3,3),bmp);
//pixel-colors into quad-grid
c:=ord(_qp_verlauf11);
for y:=0 to 2 do begin
pba:=pixelbmp.scanline[y];
for x:=0 to 2 do begin
hauptf.qp_sg.cells[c,r]:=
inttostr(hauptf.pba2col(pba,x));
inc(c);
end;
end;
end;
var
r,rr:integer;
jpg:tjpegimage;
bmp,pixelbmp:tbitmap;
begin
if hauptf.qp_neub.Caption='Neu' then begin
screen.cursor:=crhourglass;
hauptf.qp_neub.Caption:='STOPP';
qp_clrsg;
jpg:=tjpegimage.Create;
bmp:=tbitmap.Create;
pixelbmp:=tbitmap.Create;
try
//pixelbmp-dimension 3 x 3
pixelbmp.PixelFormat:=pf24bit;
pixelbmp.Width:=3;
pixelbmp.Height:=3;
//bitmap-holder fuer jpg
bmp.PixelFormat:=pf24bit;
hauptf.qp_prgb.Max:=hauptf.qp_flb.items.count-1;
rr:=1;
for r:=0 to hauptf.qp_flb.items.count-1 do begin
try
hauptf.qp_flb.itemindex:=r;
hauptf.qp_prgb.position:=r;
application.processmessages;
if hauptf.qp_neub.Caption<>'STOPP' then
break;
//read pic
jpg.Scale:=jsfullsize;
jpg.LoadFromFile(
hauptf.qp_dlb.directory+'\'+
hauptf.qp_flb.items[r]
);
//set quality (depends on size)
jpg.Scale:=optscale(jpg);
//convert to bmp
bmp.Assign(jpg);
//calculate middle-color
hauptf.qp_sg.cells[ord(_qp_farbe),rr]:=
inttostr(
qp_middlecolor(
bmp,
0,0,bmp.Width,bmp.height
)
);
hauptf.qp_sg.cells[ord(_qp_nr),rr]:=inttostr(rr);
hauptf.qp_sg.cells[ord(_qp_fn),rr]:=hauptf.qp_flb.items[r];
//set smooth-colors
setverlaufcolors(bmp,pixelbmp,rr);
inc(rr);
except
//ignore bad pics
end;
end;
//adapt quad-grid-rowcount
if rr=1 then rr:=2;
hauptf.qp_sg.RowCount:=rr;
//save quad-grid
qp_wrsg;
//build histogram
qp_mkhistoimg;
//check first entry
hauptf.qp_sg.Row:=1;
qp_sgClick;
//clean up
hauptf.qp_prgb.position:=0;
hauptf.qp_neub.Caption:='Neu';
finally
pixelbmp.free;
bmp.Free;
jpg.Free;
op_updateprev;
hauptf.setbuttons;
screen.cursor:=crdefault;
end;
end
else begin
//stopp-button clicked
hauptf.qp_neub.Caption:='Neu';
end;
end;
//set head of quad-grid-----------------
procedure qp_setheadsg;
begin
hauptf.qp_sg.Cells[ord(_qp_nr),0]:='Nr';
hauptf.qp_sg.Cells[ord(_qp_fn),0]:='Bild';
hauptf.qp_sg.Cells[ord(_qp_farbe),0]:='Mittel';
hauptf.qp_sg.Cells[ord(_qp_verlauf11),0]:='Verlauf';
end;
//delete quad-grid-------------------------
procedure qp_clrsg;
var
c:integer;
begin
for c:=0 to ord(_qp_c)-1 do
hauptf.qp_sg.cols[c].Clear;
hauptf.qp_sg.rowcount:=2;
hauptf.qp_sg.ColCount:=ord(_qp_verlauf11)+1;
qp_setheadsg;
//delete histogram
qp_mkhistoimg;
//new quad-preview
qp_sgClick;
hauptf.setbuttons;
end;
Was wird hier gemacht?
In der Prozedur "qp_fillsg" stellen wir zunächst fest, ob das Füllen der
Quader-StringGrid "qp_sg" gestartet oder beendet werden soll. Das lässt sich
anhand des "Neu"-Buttons ermitteln. Der wechselt nämlich die Caption je nach
Aktion von "Neu" auf "STOPP" und umgekehrt.
Soll die StringGrid gefüllt werden, so durchlaufen wir alle Einträge der
FileListBox "qp_flb". Wir setzen jeweils die ProgressBar auf die neue Position
und prüfen, ob es inzwischen zu einer manuellen Unterbrechung gekommen ist.
Falls nicht, laden wir das aktuelle Quader-Bild in "jpg" ein. Je nach Grösse
des Bildes wird die Skalierung von "jpg" geändert. Dies regeln wir über die interne
Funktion "optscale". Es gilt: Je kleiner das Bild ist, um so höher muss seine
Qualität bleiben, damit die einzelnen Farbwerte nicht zu sehr verfälscht werden.
Natürlich könnte man generell mit der höchsten Qualitätsskalierung arbeiten.
Das würde aber den Scan-Vorgang bei grossen Bildern erheblich verlangsamen,
ohne nennenswert bessere Ergebnisse zu erbringen.
Bei der Skalierung wird die Dimension des Quader-Bildes verkleinert, sofern nicht
"jsfullsize" vorliegt. Hat ein Bild nach dem Einladen z.B. die Breite "455" Pixel und
die Höhe "341" Pixel, wird die Skalierung "jsquarter" gewählt, wodurch das resultierende
Bild auf "114" Pixel Breite und "86" Pixel Höhe verkleinert wird. Das beschleunigt
natürlich alle weiteren Aktionen mit diesem Image.
Im nächsten Schritt wird das "jpg"-Image in eine Bitmap "bmp" kopiert. Das ist nötig,
damit wir auf die einzelnen Farbwerte des Bildes zugreifen können; dies ist generell
nur bei Bitmaps möglich.
Die Bitmap wird an die Prozedur "qp_middlecolor" übergeben, die uns dann deren
Mittel-Farbe zurück liefert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
//middlecolor of 'inner' bitmap
function qp_middlecolor(
bmp:tbitmap;
l,t,w,h:integer
):tcolor;
var
anz,x,y:integer;
//col:tcolor;
rc,gc,bc:int64;
pba:pbytearray;
begin
rc:=0;gc:=0;bc:=0;
for y:=t to t+h-1 do begin
pba:=bmp.scanline[y];
for x:=l to l+w-1 do begin
//add colors of red,green,blue
rc:=rc+pba[x*3+2];
gc:=gc+pba[x*3+1];
bc:=bc+pba[x*3+0];
end;
end;
//calculate middle of color-sums
anz:=w*h;
rc:=round(rc/anz);
gc:=round(gc/anz);
bc:=round(bc/anz);
//give back middle color
result:=rgb(rc,gc,bc);
end;
Die Bitmap wird dazu zeilenweise durchlaufen. Wie bereits erwähnt, liefert uns die
Scanline-Funktion jeweils eine komplette Zeile der Bitmap in einem PByteArray
zurück. Dieses PByteArray gehen wir nun "pixelweise" durch, wobei jedes Pixel
durch drei PByteArray-Werte repräsentiert wird, die die Werte der Farbkanäle
rot, grün und blau enthalten. Die summieren wir einzeln auf. Danach teilen wir
diese Summen durch die Anzahl der Pixel in der Bitmap und erhalten so die
Durchschnittswerte der Farbkanäle über die gesamte Bitmap. Zuletzt machen wir
daraus wieder einen "Gesamtfarbwert" und liefern diesen zurück.
Wieder in der "qp_fillsg"-Prozedur tragen wir den gerade ermittelten "Gesamtfarbwert"
als Mittel-Farbe in die Quader-StringGrid "qp_sg" ein. Ausserdem merken wir uns hier
den Namen des Quader-Bildes.
Über die Mittel-Farbe des Quader-Bildes kann Pic-of-Pics später prüfen, ob sich
ein Quader-Bild als Repräsentant für ein Pixel des Originalbildes eignet. Je
ähnlicher die Farbwerte sind, umso besser natürlich das Ergebnis.
Noch eine Stufe weiter geht der "Verlaufsmodus". Hier werden jeweils 3 x 3
Pixelblöcke des Originalbildes mit 3 x 3 Verlaufsfarben der Quader-Pics
verglichen. Die Verlaufsfarben der Quader-Bilder ermitteln wir in einem nächsten
Schritt über die interne Prozedur "setverlaufcolors".
Hier wird die Quader-Bitmap "bmp" zunächst über die "StretchDraw"-Methode
des Canvas in eine 3 x 3 Pixel grosse "pixelbmp" verkleinert. Windows
erledigt dabei für uns den schwierigen Job, das Original möglichst exakt in
die verkleinerte Form zu transferieren. Dann holen wir uns einfach alle 9 Farbwerte
der "pixelbmp" und sichern sie als Verlaufsfarben in der Quader-StringGrid "qp_sg".
Verlaufsfarben: Quader-Bilder als 3 x 3 Pixel-Bitmaps. Man sieht hier eine sehr vereinfachte Wiedergabe der Bilder von Jessica Alba.
Auf der linken Seite der Quader-Pool-Page finden wir eine DriveComboBox, ein
DirectoryListBox und die FileListBox "qp_flb". Über diese Komponenten wird der
Ordner ausgewählt, der die Quader-Bilder enthält.
Das sind reichlich antiquierte Form-Elemente. In meiner ursprünglichen Version
von PicOfPics hatte ich hier etwas Schöneres verwendet. Doch da das Tutorial
für jedermann gedacht ist und nicht jeder Willens ist, sich neue Komponenten
in sein Delphi-System zu installieren, finden sich hier nun diese Standards.
Der Schönheit der fertigen Collagen schadet dies ja letztlich nichts.
Klickt man in die FileListBox "qp_flb" wird das Ereignis "OnChange" ausgelöst,
was wiederum die Funktion "qp_ldimg" aufruft:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
//load quad pic -------------------------
procedure qp_ldimg;
var
r:integer;
fn:string;
begin
try
hauptf.qp_img.Picture.Graphic:=nil;
r:=hauptf.qp_flb.ItemIndex;
if r=-1 then exit;
fn:=hauptf.qp_flb.Items[r];
try
//file in quad-grid?
for r:=1 to hauptf.qp_sg.RowCount-1 do begin
if hauptf.qp_sg.cells[ord(_qp_fn),r]<>fn then
continue;
//mark entry-row
hauptf.qp_sg.col:=ord(_qp_fn);
hauptf.qp_sg.row:=r;
break;
end;
//read from disk
hauptf.qp_img.picture.loadfromfile(
hauptf.qp_dlb.Directory+'\'+fn
);
except
//shit happens
hauptf.qp_img.Picture.Graphic:=nil;
end;
finally
//set 'cursor' on histogram
qp_sethistocursor;
end;
end;
Hier wird zunächst der Name der angeklickten Datei ermittelt.
Dann wird geprüft, ob sich das Bild bereits in der Quader-StringGrid
befindet. Falls ja, wird der entsprechende Eintrag markiert.
Anschliessend wird das Bild in "qp_img" eingeladen und als Vorschau
angezeigt.
Zuletzt wird die Funktion "qp_sethistocursor" aufgerufen. Die sorgt dafür,
dass der "Helligkeitswert" des Quader-Bildes im Histogramm als "Cursor-Linie"
eingezeichnet wird.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
//set 'cursor' on histogram --------------
//(gray-value of actually quad-pic)
procedure qp_sethistocursor;
var
x,helligkeit,hfg:integer;
bmp:tbitmap;
col:tcolor;
hfgproz:double;
s:string;
begin
hauptf.qp_sh.brush.color:=clsilver;
hauptf.qp_helle.Text:='';
hauptf.qp_haeufe.Text:='';
if qp_isempty then exit;
bmp:=tbitmap.create;
try
//get original histogram
bmp.assign(hauptf.qp_histobmp);
//get middle-color
col:=strtoint(
hauptf.qp_sg.cells[ord(_qp_farbe),hauptf.qp_sg.Row]
);
hauptf.qp_sh.Brush.color:=col;
//calculate gray-value
helligkeit:=hauptf.col2helligkeit(col);
hauptf.qp_helle.Text:=inttostr(helligkeit);
//number of that gray-value in histogram
hfg:=trunc(hauptf.qp_histoa[helligkeit]);
s:=inttostr(hfg);
hfgproz:=(hfg*100)/(hauptf.qp_sg.RowCount-1);
s:=s+' ('+format('%f',[hfgproz])+'%)';
hauptf.qp_haeufe.Text:=s;
//'cursor'-line for histogram
x:=helligkeit;
bmp.canvas.pen.width:=1;
bmp.canvas.pen.color:=clgreen;
bmp.Canvas.MoveTo(x,0);
bmp.Canvas.lineTo(x,bmp.height);
finally
//show (new) histogram
hauptf.qp_histoimg.picture.assign(bmp);
bmp.free;
end;
end;
Dazu wird zunächst die originale Histogramm-Bitmap aus "qp_histobmp" in "bmp"
kopiert. "Original" deshalb, weil diese Bitmap das berechnete Histogramm ohne
eingezeichnete Cursor-Linie enthält. Wir wir die "qp_histobmp" genau generieren,
sehen wir später noch.
Aus der Quader-StringGrid erhalten wir die Mittel-Farbe des angeklickten
Quader-Bildes. Wir wandeln diese Farbe nun mit der uns bereits bekannten Funktion
"col2helligkeit" in einen Helligkeitswert (Grauwert) um.
Jetzt noch etwas Arithmetik und wir wissen, an welcher x-Koordinate sich der Cursor
in der von 0 bis 255 reichenden Graustufen-Skala des Histogramms befinden muss.
An dieser Stelle tragen wir eine grüne Linie über die komplette Höhe des Histogramms
ein und kopieren die Hilf-Bitmap "bmp" in "qp_histoimg", um sie so zur Anzeige zu
bringen.
Okay, über die Sinnhaftigkeit des Histogramm-Cursors kann man streiten. Aber das
bringt immerhin etwas Leben in die Quader-Pool-Page :-)
Histogramm-Cursor I: Dark Angle Jessica Alba - die Farben sind sehr dunkel gehalten. Entsprechend steht der Cursor des Histogramms ziemlich weit links.
Histogramm-Cursor II: Hell wie ein Stern erscheint uns hier Jessica. Der Cursor des Histogramms ist daher diesmal weit auf der rechten Seite zu finden.
Rechts in der Quader-Pool-Page befindet sich die Quader-StringGrid. Sie enthält
die Namen der gescannten Quader-Bilder, ihre Mittel-Farben" und ihre 3 x 3
Verlaufsfarben.
Auch wenn es nicht so aussieht: Die Farbwerte sind tatsächlich als Strings in
den einzelne Spalten der StringGrid eingetragen worden. Der Index der Spalten ist
folgendermassen definiert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
type
//index for quad-stringgrid
_qp_inx=(
_qp_nr,
_qp_fn,
_qp_farbe,
_qp_verlauf11,
_qp_verlauf21,
_qp_verlauf31,
_qp_verlauf12,
_qp_verlauf22,
_qp_verlauf32,
_qp_verlauf13,
_qp_verlauf23,
_qp_verlauf33,
_qp_c
);
Da nun aber die Farbwerte als reine Zahlen wenig informativ sind (ausser
für die paar Helden, die in der "Matrix" lesen können), werden sie im
"OnDrawCell"-Ereignis der Quader-StringGrid "qp_sg" in eine optisch ansprechendere
Form transferiert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
//painting the quad-grid-cells -------------------------
procedure qp_sgDrawCell(
Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState
);
var
s:string;
c,l,t,w,x,y:integer;
cnv:tcanvas;
rec:trect;
dw,dh:double;
begin
cnv:=hauptf.qp_sg.Canvas;
s:=hauptf.qp_sg.cells[acol,arow];
try
//background color
if state=[] then begin
//not marked: set own color
if acol=ord(_qp_fn) then
cnv.brush.color:=clwhite
else if acol=ord(_qp_farbe) then
cnv.brush.color:=strtoint(s)
else if acol=ord(_qp_verlauf11) then begin
//paint a 3x3 color pattern
c:=ord(_qp_verlauf11);
dw:=(rect.Right-rect.Left)/3;
dh:=(rect.Bottom-rect.Top)/3;
for y:=0 to 2 do begin
for x:=0 to 2 do begin
cnv.brush.color:=strtoint(
hauptf.qp_sg.cells[c,arow]
);
rec.Left:=rect.Left+trunc(x*dw);
rec.Right:=rec.left+trunc(dw);
rec.top:=rect.top+trunc(y*dh);
rec.bottom:=rec.top+trunc(dh);
cnv.FillRect(rec);
inc(c);
end;
end;
exit;
end;
end;
cnv.FillRect(rect);
//dont show values of color-entries
if(arow>0)and(acOL>=ord(_qp_farbe))then exit;
//text-aligment
if acol=ord(_qp_nr) then begin
//right
w:=cnv.textwidth(s);
l:=rect.Right-w-4;
end
else begin
//left
l:=rect.left+2;
end;
//place it in the middle
t:=
rect.Top+
(
(
rect.Bottom-
rect.Top-
cnv.textheight(s)
) div 2
);
//paint text
cnv.TextOut(l,t,s);
except
end;
end;
Wir prüfen, in welcher Zeile und Spalte wir uns befinden.
Bei den ersten beiden Spalten (Nr und Quader-Name) wird der Zelleninhalt
einfach mit der Canvas-Funktion "textout" ausgegeben. Befinden wir uns in
der "Mittelfarbe"-Spalte, wandeln wir den Inhalt zum Farbwert und setzen
die Zellen-Hintergrund-Farbe über den Canvas-Brush auf den gleichen Wert.
Wird dagegen die erste Verlaufsfarben-Spalte bearbeitet, dann verwenden
wir die Inhaltswerten der letzten 9 Spalten, um damit eine 3 x 3 Grafik
auf dem Canvas der Zelle auszugeben. Da die Eigenschaft "ColCount" von
"qp_sg" künstlich auf 4 herabgesetzt wurde, werden alle restlichen
"virtuellen" Spalten bei der Anzeige ignoriert.
PicOfPics speichert den jeweils zuletzt ermittelten Quader-Pool als
Textfile "qp.txt" im Arbeitsordner. Als Delimiter wird "|" verwendet.
Beim Neustart wird die Datei wieder eingelesen, wobei je Eintrag geprüft
wird, ob die zugehörige Datei physikalisch noch vorhanden ist.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
//reading quader-sg from disk------------------------
procedure qp_rdsg;
var
r,c,cc:integer;
tf:textfile;
fn,s,ss:string;
begin
//reset quad-pool.grid
qp_clrsg;
//read quad-sg from file
if fileexists(hauptf.homedir+_qpfn)then begin
r:=0;
assignfile(tf,hauptf.homedir+_qpfn);
try
reset(tf);
while not eof(tf) do begin
readln(tf,s);
for c:=0 to ord(_qp_c) do begin
cc:=pos('|',s);if cc=0 then cc:=length(s)+1;
ss:=copy(s,1,cc-1);
hauptf.qp_sg.cells[c,r]:=ss;
s:=copy(s,cc+1,length(s));
end;
inc(r);
end;
finally
closefile(tf);
end;
//adapt rowcount of quad-grid
if r<2 then r:=2;
hauptf.qp_sg.rowcount:=r;
//check quad-pics <-> picture-liste in directory
for r:=hauptf.qp_sg.rowcount-1 downto 1 do begin
fn:=
hauptf.qp_dlb.directory+'\'+
hauptf.qp_sg.cells[ord(_qp_fn),r];
//ok?
if fileexists(fn) then continue;
//no - quad does not exitsts
if hauptf.qp_sg.rowcount>2 then begin
//del row in quad-grid
hauptf.qp_sg.rows[r].clear;
end
else begin
//del entry, but keep the row
for c:=0 to ord(_qp_c)-1 do
hauptf.qp_sg.cells[c,r]:='';
end;
end;
//quad-grid totally empty?
if qp_isempty then qp_clrsg;
//save to disk
qp_wrsg;
//show histogram
qp_mkhistoimg;
qp_sgclick;
end;
end;
//write quad-grid to disk--------------------
procedure qp_wrsg;
var
tf:textfile;
r,c:integer;
s:string;
begin
//del previous quad-pool
deletefile(hauptf.homedir+_qpfn);
//if quad-grid is empty, nothing to do
if qp_isempty then exit;
//save grid as textfile
assignfile(tf,hauptf.homedir+_qpfn);
try
rewrite(tf);
for r:=0 to hauptf.qp_sg.RowCount-1 do begin
s:='';
for c:=0 to ord(_qp_c)-1 do begin
s:=s+hauptf.qp_sg.cells[c,r]+'|';
end;
writeln(tf,s);
end;
finally
closefile(tf);
end;
end;
Im unteren Teil der Quader-Pool-Page zeigt ein Histogramm, welcher
Helligkeitsbereich mit den Quader-Bildern abgedeckt wird. Die
"Qualität" des Pools ist allgemein umso besser, je breiter das
Spektrum ist. Für den Einzelfall wichtiger ist jedoch, dass sich
das Histogramm einigermassen mit dem des Originalbildes deckt.
Generiert wird das Histogramm des Quader-Pools mittels der Prozedur
"qp_mkhistoimg". Um die Sache übersichtlich zu halten, wird nur die
Mittel-Farbe der Quader beachtet, nicht deren Verlaufsfarben.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
//create histogram over all quad-pics -----------------
procedure qp_mkhistoimg;
var
ca:array[0..256]of double;
c,r,helligkeit:integer;
col:tcolor;
begin
//reset gray-color-array
for c:=0 to 255 do ca[c]:=0;
//quad loaded?
if not qp_isempty then begin
for r:=1 to hauptf.qp_sg.rowcount-1 do begin
//get color-entry of quad-grid
col:=tcolor(
strtoint(
hauptf.qp_sg.cells[ord(_qp_farbe),r]
)
);
//convert color-value to gray-color
helligkeit:=hauptf.col2helligkeit(col);
//increase counter in gray-array
ca[helligkeit]:=ca[helligkeit]+1;
end;
//save back values of gray-array
for c:=0 to 255 do
hauptf.qp_histoa[c]:=ca[c];
//want a logarithm norm of values?
if hauptf.qp_logchb.checked then begin
for c:=0 to 255 do
ca[c]:=ln(1+ca[c]);
end;
end;
//transfer gray-array to bitmap
hauptf.mkhistoimg(
hauptf.qp_histoimg,ca
);
//save histogram
hauptf.qp_histobmp.assign(
hauptf.qp_histoimg.Picture
);
end;
Zunächst werden die 255 Werte des Array "ca" auf null gesetzt.
Dann durchlaufen wir den Quader-Pool, holen uns die jeweilige
Mittel-Farbe, wandeln sie in einen Grauwert um, der ja von 0
bis 255 reichen kann, und benutzen diesen Grauwert als Index
für den Array-Eintrag, der anschliessend inkrementiert wird.
So steht am Schluss in "ca", wie oft welcher Grauwert im Quader-Pool
gefunden wurde.
Optional können die Histogramm-Werte "normiert" werden, um die
Dominanz bestimmter Helligkeitswerte zu relativieren. Das sehen
wir uns gleich noch einmal beim Originalbild näher an.
Jetzt wenden wir uns der Verwaltung desjenigen Bildes zu, welches
durch die eben bestimmten Quader "nachgebaut" werden soll.
Page des Originalbildes: Oben kann ein neues Bild per Öffnen-Dialog eingeladen werden. Unten zeigt ein Histogramm die Häufigkeit der darin enthalten Helligkeitswerte. Das gewählte Beispielbild von Jessica Alba weist dabei eine recht ausgeglichen Verteilung auf.
Das Einladen geschieht über die Prozedur "ob_rdimg":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
//remove original-pic-------------------------
procedure ob_entfernen;
begin
hauptf.ob_img.Picture.Graphic:=nil;
hauptf.ob_img.Hint:='Kein Quellbild';
hauptf.ob_fne.Text:='';
ob_mkhistoimg;
op_updateprev;
hauptf.setbuttons;
end;
//check if original pic is loaded
function ob_isempty:bool;
begin
result:=(hauptf.ob_img.Picture.Graphic=nil);
end;
//load original-image from file------------------------
procedure ob_rdimg(fn:string);
begin
screen.Cursor:=crhourglass;
try
//remove previous
ob_entfernen;
if not fileexists(fn) then exit;
try
//get new one
hauptf.ob_img.Picture.loadfromfile(fn);
hauptf.ob_fne.text:=fn;
//save original to bmp
hauptf.ob_bmp.assign(
hauptf.ob_img.Picture.graphic
);
//set hint informations
hauptf.ob_img.Hint:=
'Original-Bild '+hauptf.ob_fne.text+_cr+
'Dimension: '+
inttostr(hauptf.ob_bmp.width)+
' x '+
inttostr(hauptf.ob_bmp.Height)+
' Pixel';
except
end;
finally
//make histogram und preview
ob_mkhistoimg;
op_updateprev;
hauptf.setbuttons;
screen.Cursor:=crdefault;
end;
end;
Zuerst wird ein eventuell vorhandenen Originalbild entfernt,
dann das Bild über die "LoadFromFile"-Methode des TImage "ob_img"
eingeladen. Für die weitere Arbeit wird das Original in der Bitmap
"ob_bmp" gesichert. Zuletzt wird das zugehörige Histogramm erstellt
und in der Optionen-Page die Vorschau neu generiert.
Das Histogram des Originalbildes wird ähnlich erstellt, wie das des
Quader-Pools. Nur dass diesmal nicht die Helligkeitswerte einzelner Mittel-Farben
von Quadern verwendet werden, sondern die Helligkeitswerte jedes einzelnen
Pixels des Originalbildes:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
//create gray-value-histogram of original
procedure ob_mkhistoimg;
var
x,y,c,helligkeit:integer;
col:tcolor;
ca:array[0..255]of double;
pba:pbytearray;
begin
//reset gray-value-array
for c:=0 to 255 do ca[c]:=0;
if hauptf.ob_bmp.height>0 then begin
//fill gray-array with numbers
//of gray-values in original-bmp
for y:=1 to hauptf.ob_bmp.height-1 do begin
pba:=hauptf.ob_bmp.ScanLine[y];
for x:=1 to hauptf.ob_bmp.width-1 do begin
//get pixel-color
col:=hauptf.pba2col(pba,x);
//convert to gray
helligkeit:=hauptf.col2helligkeit(col);
//increase gray-value in array
ca[helligkeit]:=ca[helligkeit]+1;
end;
end;
//save gray-array
for c:=0 to 255 do hauptf.ob_histoa[c]:=ca[c];
//want logarithm norm?
if hauptf.ob_logchb.checked then begin
for c:=0 to 255 do ca[c]:=ln(1+ca[c]);
end;
end;
//transfer gray-array to histogram-image
hauptf.mkhistoimg(hauptf.ob_histoimg,ca);
//save histogram
hauptf.ob_histobmp.assign(hauptf.ob_histoimg.Picture);
end;
Da in einigen Bilder bestimmte Helligkeitswerte stark dominieren
können, führt dies unter Umständen zu Histogrammen, deren Aussagekraft
eingeschränkt wird, weil feinere Abstufungen nicht mehr zu erkennen
sind.
Histogramm unnormiert: Die Farbe Weiss dominiert in obigem Bild deutlich. Andere Graustufen sind nicht oder nur kaum zu erkennen. Das macht es schwierig abzuschätzen, welche Thumbnails als Mosaike einzusetzen sind.
Um diesen Effekt abzumildern, können die Histogramm-Werte optional normiert werden,
indem die "Ausreisser" mittels einer Logarithmus-Funktion "geglättet" werden:
Histogramm normiert: Das Helligkeitsspektrum in normierter Version lässt feinere Abstufungen erkennen.
Ähnlich wie beim Quader-Pool wollen wir auch in das Histogramm des Originalbildes
einen "Cursor" einzeichnen. Er soll den Helligkeitswert anzeigen, den das Pixel
besitzt, über dem wir uns mit der Maus gerade befinden. Dazu fangen wir das
TImage-Ereignis "OnMouseMove" ab:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
//mouse moves on original image----------------------
//set 'cursor' in histogram
procedure ob_imgMouseMove(
Sender: TObject;
Shift: TShiftState;
X,Y: Integer
);
var
l,t,w,h,
helligkeit,hfg:integer;
bmp:tbitmap;
col:tcolor;
d,hfgproz:double;
s:string;
begin
hauptf.ob_sh.brush.color:=clsilver;
hauptf.ob_helle.Text:='';
hauptf.ob_haeufe.Text:='';
if ob_isempty then exit;
bmp:=tbitmap.create;
try
//get backup of histogram
bmp.assign(hauptf.ob_histobmp);
//calculate position of 'inner' image in PaintBox
hauptf.getinnerbounds(
hauptf.ob_img.Width,
hauptf.ob_img.height,
hauptf.ob_bmp.Width,
hauptf.ob_bmp.height,
l,t,w,h
);
//mouse over image?
x:=x-l;if(x<0)or(x>w)then exit;
y:=y-t;if(y<0)or(y>h)then exit;
//convert position to original-image
d:=hauptf.ob_bmp.Width/w;
x:=trunc(x*d);
y:=trunc(y*d);
//get color-value under mouse
col:=hauptf.ob_bmp.canvas.Pixels[x,y];
hauptf.ob_sh.Brush.color:=col;
//convert to gray-value
helligkeit:=hauptf.col2helligkeit(col);
hauptf.ob_helle.Text:=inttostr(helligkeit);
//show number of gray-values in hitogram-array
hfg:=trunc(hauptf.ob_histoa[helligkeit]);
s:=inttostr(hfg);
hfgproz:=(hfg*100)/(hauptf.ob_bmp.Width*hauptf.ob_bmp.height);
s:=s+' ('+format('%f',[hfgproz])+'%)';
hauptf.ob_haeufe.Text:=s;
//paint 'cursor'-line in histogram
x:=helligkeit;
bmp.canvas.pen.width:=1;
bmp.canvas.pen.color:=clgreen;
bmp.Canvas.MoveTo(x,0);
bmp.Canvas.lineTo(x,bmp.height);
finally
//show (new) histogram
hauptf.ob_histoimg.picture.assign(bmp);
bmp.free;
end;
end;
Es muss ermittelt werden, über welchem Pixel wir uns mit der Maus
befinden. Dazu wird zunächst berechnet, ab wo das im Image-Bereich
zentrierte "innere" Bild eigentlich beginnt. Diese Information erhalten
wir von der bereits beschriebene Funktion "getinnerbounds".
Befinden wir uns mit der Maus im "Aus", wird nichts weiter gemacht.
Befinden wir uns jedoch über dem "inneren" Bild, so rechnen wir nun die
aktuellen Mauskoordinaten auf das Originalbild hoch - denn das kann ja
grösser oder kleiner sein als das proportional angepasst angezeigte Bild.
Haben wir die konvertierten Koordinaten, können wir den Farbwert des
entsprechenden Pixels im Originalbild holen. Umgewandelt in einen Grauwert,
der als Index im Histogramm-Array verwendet wird, lassen sich weitere
Informationen gewinnen. Zuletzt wird der "Cursor" an passender Stelle in
das Histogramm eingezeichnet.
Histogramm-Cursor: Auch wenn es hier nicht zu sehen ist - der Mauszeiger befindet sich gerade über Victoria Beckhams dunklem Haar. Entsprechend weit links ist der Histogramm-Cursor eingezeichnet worden.
Bleibt noch eine Prozedur zu beschreiben, nämlich "ob_mkpixelbmp". Hier wird das
Originalbild in "komprimierter" Form auf die Bitmap "ob_pixelbmp" kopiert.
Sie bekommt die Dimension, die durch die in der Optionen-Page vorgegebene Anzahl
horizontaler Quader vorgegeben ist. So steht jedes Pixel der "ob_pixelbmp" für ein
Quader-Bild im späteren Ergebnisbild. Sollte der "Verlaufsmodus" aktiv sein,
verdreifachen sich entsprechend Breite und Höhe, da nun jeweils 3 x 3 Pixel für
ein Quader-Bild stehen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
//create pixelbmp: reduce original to quad-dimension
//=> every pixel represents one quader
procedure ob_mkpixelbmp(prevok:bool);
var
rec:trect;
w,h:integer;
begin
//set options-infos
op_u.op_setebinfo;
//is there an original-pic
if ob_isempty then exit;
//get dimension of quads
w:=hauptf.op_qhorzse.value;
h:=strtoint(hauptf.op_qverte.text);
//just preview?
if not prevok then begin
//no: want smooth-mode?
if hauptf.op_verlaufchb.checked then begin
//yep, increase dimension
//(smooth needs 3x3 pixels for one quader)
w:=w*3;
h:=h*3;
end;
end;
//set dimension of pixelbmp
hauptf.ob_pixelbmp.width:=w;
hauptf.ob_pixelbmp.Height:=h;
//reduce original to pixelbmp
if prevok or (w<4) then begin
//fast stretch
rec:=rect(0,0,w,h);
hauptf.ob_pixelbmp.Canvas.StretchDraw(rec,hauptf.ob_bmp);
end
else begin
//optimized thumb
op_u.op_optthumb(hauptf.ob_bmp,hauptf.ob_pixelbmp);
end;
end;
Das grundsätzliche Aussehen des Ergebnisbildes wird durch die Einstellungen
in der Optionen-Page vorgegeben: Originalbild als Hintergrundbild, Anzahl
horizontaler Quader, die Breite jedes Quaders in Pixeln, Verlaufsmodus,
Verwacklungsmodus, Quader-Rand, Mal-Wahrscheinlichkeit, Qualität der Quader usw.
Optionen-Page: Dieses Register bietet dem Anwender viele Schrauben zum Drehen, um die Mosaik-Wallpaper den eigenen Wünschen anzupassen.
Fast alle Änderungen an den Optionen bewirken die sofortige Neuberechnung
des Vorschau-Bildes über die Prozedur "op_mkprevbmp". Wie wir noch sehen
werden, arbeitet diese Prozedur ähnlich wie die "echte" Pic-of-Pics-Prozedur,
nur dass statt Quader-Bilder einfach passend gefärbte Rechtecke verwendet
werden (tatsächlich wäre es möglich und sinnvoll gewesen, Vorschau und
Originalbild mit der gleichen Prozedur zu generieren, aber als ich endlich
auf diese Idee kam, war's schon zu spät, als dass mir das noch Vorteile
gebracht hätte).
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
//make preview-bmp: original width quads as pixels
procedure op_mkprevbmp(zbmp:tbitmap);
//convert int to byte------------
function i2b(i:integer):byte;
begin
if i<0 then i:=0;
if i>255 then i:=255;
result:=byte(i);
end;
var
ql,qt,qw,qh,
ww,hh,
l,t,w,h,
x,y,
helligkeit,
vy,
yanz:integer;
dx,dy,dvy:double;
col:tcolor;
in_hellbereich:bool;
pba:pbytearray;
r,g,b,rc,gc,bc:byte;
begin
if not hauptf.visible then exit;
//for optimation
inc(hauptf.cc_updateprevc);hauptf.countercheck;
//set preview-bmp dimension to PaintBox-dimension
hauptf.op_prevbmp.Width:=hauptf.op_prevpb.width;
hauptf.op_prevbmp.height:=hauptf.op_prevpb.height;
//paint plane background
hauptf.op_prevbmp.Canvas.Brush.color:=clsilver;
hauptf.op_prevbmp.Canvas.FillRect(
rect(
0,
0,
hauptf.op_prevbmp.width,
hauptf.op_prevbmp.height
)
);
//preview mode?
if not hauptf.op_prevchb.checked then exit;
//is there an original-pic?
if ob_u.ob_isempty then exit;
//get dimension of inner preview-pic in PaintBox
hauptf.getinnerbounds(
hauptf.op_prevpb.Width,
hauptf.op_prevpb.height,
hauptf.ob_bmp.width,
hauptf.ob_bmp.height,
l,t,w,h
);
//save width and hight
ww:=w;
hh:=h;
//creating the result-pic?
if zbmp<>nil then begin
//copy actually pic to result-bitmap
hauptf.op_prevbmp.Canvas.StretchDraw(
rect(l,t,l+w,t+h),
zbmp
);
exit;
end;
//only a preview depends on options ---------------
randseed:=123;
//coNvert original to (small) pixelbmp
//=> every pixel stands for a quad
ob_u.ob_mkpixelbmp(true);
if hauptf.op_backpicchb.checked then begin
//original as background
hauptf.op_prevbmp.Canvas.StretchDraw(
rect(l,t,l+w,t+h),
hauptf.ob_bmp
);
end
else begin
//set background-color
hauptf.op_prevbmp.Canvas.Brush.color:=
hauptf.op_backsh.Brush.color;
hauptf.op_prevbmp.Canvas.FillRect(
rect(l,t,l+w,t+h)
);
end;
//paint dummy-quads on preview--------------------
//calculate width of a quad on preview
dx:=w/hauptf.op_qhorzse.value;
//calculate hight of a quad on preview
yanz:=strtoint(hauptf.op_qverte.text);
dy:=h/yanz;
//transform dimension from float to int
w:=trunc(dx);
h:=trunc(dy);
//border_color: black as default
hauptf.op_prevbmp.Canvas.brush.style:=bssolid;
hauptf.op_prevbmp.Canvas.pen.width:=1;
hauptf.op_prevbmp.Canvas.pen.color:=clblack;
for y:=0 to yanz-1 do begin
//calc top of actually quad on preview
qt:=trunc(y*dy);
//get line of colors
pba:=hauptf.ob_pixelbmp.scanline[y];
for x:=0 to hauptf.op_qhorzse.value-1 do begin
//set quad randomly
if random(100)>hauptf.op_malwkse.value then continue;
//calc left of actually quad on preview
ql:=trunc(x*dx);
//get pixel-color
col:=hauptf.pba2col(pba,x);
//is gray-value of the color in chosen limits?
helligkeit:=hauptf.col2helligkeit(col);
in_hellbereich:=
(helligkeit>=hauptf.op_hellvonsb.position)and
(helligkeit<=hauptf.op_hellbissb.position);
if
((hauptf.op_hellmodecb.itemindex=0)and not in_hellbereich)or
((hauptf.op_hellmodecb.itemindex=1)and in_hellbereich)
then continue;
//quader dimension randomazation
ql:=ql+hauptf.verwackeln(w,hauptf.op_wxse.value);
qt:=qt+hauptf.verwackeln(h,hauptf.op_wyse.value);
qw:=w+hauptf.verwackeln(w,hauptf.op_wwse.value);
qh:=h+hauptf.verwackeln(h,hauptf.op_whse.value);
if hauptf.op_verlaufchb.Checked then begin
//no border? Then border color=brush color
if not hauptf.op_qrandchb.checked then
hauptf.op_prevbmp.Canvas.pen.Color:=col;
r:=getrvalue(col);
g:=getgvalue(col);
b:=getbvalue(col);
dvy:=qh/3;
for vy:=0 to 2 do begin
rc:=i2b(r+random(30)-random(30));
gc:=i2b(g+random(30)-random(30));
bc:=i2b(b+random(30)-random(30));
col:=rgb(rc,gc,bc);
hauptf.op_prevbmp.Canvas.brush.Color:=col;
hauptf.op_prevbmp.Canvas.pen.Color:=col;
hauptf.op_prevbmp.Canvas.rectangle(
l+ql,t+qt+trunc(vy*dvy),
l+ql+qw,t+qt+trunc(vy*dvy+dvy)
);
end;
if hauptf.op_qrandchb.checked then begin
hauptf.op_prevbmp.Canvas.pen.color:=clblack;
hauptf.op_prevbmp.Canvas.brush.style:=bsclear;
hauptf.op_prevbmp.Canvas.rectangle(
l+ql,t+qt,l+ql+qw,t+qt+qh
);
hauptf.op_prevbmp.Canvas.brush.style:=bssolid;
end;
end
else begin
//no border? Then border color=brush color
if not hauptf.op_qrandchb.checked then
hauptf.op_prevbmp.Canvas.pen.Color:=col;
hauptf.op_prevbmp.Canvas.brush.Color:=col;
//paint the dummy-quad on preview
hauptf.op_prevbmp.Canvas.rectangle(
l+ql,t+qt,l+ql+qw,t+qt+qh
);
end;
end;
end;
if not hauptf.op_wackelchb.Checked then exit;
//to repaint quads out of inner frame
hauptf.op_prevbmp.Canvas.Brush.color:=clsilver;
hauptf.op_prevbmp.Canvas.FillRect(
rect(0,0,hauptf.op_prevbmp.width,t-1)
);
hauptf.op_prevbmp.Canvas.FillRect(
rect(0,0,l-1,hauptf.op_prevbmp.height)
);
hauptf.op_prevbmp.Canvas.FillRect(
rect(l+ww+1,0,hauptf.op_prevbmp.width,hauptf.op_prevbmp.height)
);
hauptf.op_prevbmp.Canvas.FillRect(
rect(0,t+hh+1,hauptf.op_prevbmp.width,hauptf.op_prevbmp.height)
);
end;
Zunächst wird die Vorschau-Bitmap "op_prevbmp" auf die Dimension der
PaintBox "op_prevpb" gesetzt, die das Vorschau-Bild letztlich anzeigt.
Dann wird die Bitmap komplett silbern eingefärbt. Ist der
Vorschau-Modus nicht aktiv oder liegt kein Originalbild vor, gibt es
nichts weiter zu tun und wir verlassen die Prozedur.
Ansonsten berechnen wir, an welchen Koordinaten sich das "innere" Bild
innerhalb der PaintBox befinden soll - ganz ähnlich, wie wir das auch
schon beim "OnMouseMove"-Ereignis des Originalbildes gemacht haben.
Nun prüfen wir, ob wir uns bereits bei der Generierung des "echten"
Ergebnisbildes befinden. In diesem Fall enthält die übergebene
Bitmap "zbmp" das bisherige Ergebnisbild, welches wir nun einfach
an passender Stelle in die Vorschau-Bitmap hinein kopieren. Anschliessend
wird die Prozedur verlassen.
'Echte' Vorschau: Die Bildcollage wird gerade berechnet, das bisher generierte Bild in der Vorschau angezeigt.
Im Vorschau-Modus müssen wir weitermachen. Wir setzen den Zufallsgenerator
auf einen definierten Startwert, sodass immer die gleichen Zufallszahlen
generiert werden. Das hat den Vorteil, dass die Wirkung der Änderungen an
den Optionen sich hinsichtlich des Ergebnisbildes besser abschätzen lässt,
weil sie nicht unnötig durch Zufallsereignisse "verschleiert" wird.
Das "innere" Bild wird je nach Einstellung entweder mit einer planen Hintergrundfarbe
oder mit dem Originalbild versehen.
Hintergrund I: Es wurde ein einfarbiger Hintergrund gewählt, hier in der Farbe Weiss.
Hintergrund II: Bei diesem Beispiel scheint das Originalbild als Hintergrund durch.
Nun wird die Bitmap "ob_pixelbmp" über die vorhin beschriebene Prozedur
"ob_mkpixelbmp" generiert, und zwar stets in der "einfachen" Variante,
d.h., der Verlaufsmodus bleibt unberücksichtigt. Die Dimension der
Pixel-Bitmap - und damit auch das Aussehen der Vorschau - wird bestimmt
durch die optionale Anzahl horizontaler Quader.
Dimension I: Unscharfes Bild mit wenigen, aber später im Ergebnisbild detaillierten Quadern.
Dimension II: Mehr Schärfe durch mehr Quader. Die Berechnung des Ergebnisbildes dauert dann deutlich länger, allerdings werden die Konturen des Originalbildes wesentlich besser abgedeckt als bei nur geringer Quader-Anzahl.
In der Folge betrachten wir jedes Pixel der "ob_pixelbmp". Je nach Einstellung
der Mal-Wahrscheinlichkeit "op_malwkse" prüfen wir, ob der Zufall will,
dass wir dieses Pixel ignorieren oder in der Vorschau in ein Rechteck umsetzen.
Der Aufruf "random(100)" liefert eine Zufallszahl zwischen 0 und 99 zurück.
Die Auswahl ist gleichverteilt, d.h., jede Zahl wird mit gleicher Wahrscheinlichkeit
"gezogen". Bei z.B. 50% Mal-Wahrscheinlichkeit wird nun einfach geprüft, ob die
Zufallszahl im Bereich 0-50 liegt. Bei 20% muss sie in dem unwahrscheinlicheren,
weil kleineren Bereich von 0-20 liegen. Bei 1% muss exakt die "1" getroffen
werden, was im Schnitt einmal bei 100 Versuchen klappt. 100% Mal-Wahrscheinlichkeit
ist immer erfüllt, da jede Zufallszahl von 0 bis 99 kleiner als 100 ist, "continue"
also nie ausgeführt wird.
Mal-Wahrscheinlichkeit I: Die Rechtecke sollen mit nur 50 prozentiger Wahrscheinlichkeit gemalt werden. Das heisst, jedes zweite Mosaik wird übersprungen; an diesen Stellen bleibt dann der gewählte Hintergrund, hier eine weisse Fläche, erhalten.
Mal-Wahrscheinlichkeit II: Standardmässig werden 100% der Rechtecke gemalt. Das heisst, es wird versucht, für jeden Bildbereich des Originalbildes passende Thumbnails zu finden.
Im nächsten Schritt bestimmen wir den Helligkeitswert "helligkeit" des aktuellen
Pixels. Liegt der innerhalb (oder ausserhalb) bestimmter, in den Optionen angegebenen
Grenzen, fahren wir entweder fort oder ignorieren einmal mehr das Pixel.
Pixel-Helligkeit I: Pixel des Originalbildes mit Helligkeitswert unter 100 (grau bis schwarz) werden bei der Thumbnail-Suche ignoriert. An diesen dunklen Stellen (hier z.B. bei den Haaren) wird demnach kein Mosaik eingebaut.
Pixel-Helligkeit II: Pixel im Originalbild mit Helligkeitswert über 150 (hellgrau bis weiss) werden ignoriert. Das heisst, alle hellen Flächen des Originalbildes werden nicht nicht mit Mosaiken versehen. Hier z.B. bliebt das komplette Gesicht unbehandelt.
Fahren wir fort, berechnen wir basierend auf der Position des aktuellen Pixels
die exakte Position und Grösse des Vorschau-Rechtecks in der Vorschau-Bitmap
"op_prevbmp". Um die optionale "Verwacklung" zu erreichen, verwenden wir die bereits
bekannte Funktion "verwackeln" aus der Haupt-Unit.
Verwacklung: Die Rechtecke werden zufällig verteilt. Das verschlechtert die Qualität der Treffer, nimmt dem fertigen Bild aber etwas von seinem statischen Aussehen.
Weiter ist zu prüfen, ob der Verlauf-Modus aktiv ist. Ist dies nämlich der Fall,
wird nicht nur ein "planes" Rechteck mit der Pixelfarbe in die Vorschau gemalt,
sondern gleich drei zusammenhängende Rechtecke mit leicht variierenden Farben.
Das gibt zwar die Wirkung des Verlauf-Modus nicht korrekt wieder, jedoch kann
man zumindest erahnen, welche Auswirkungen das auf das Ergebnisbild hat.
Verlauf-Modus inaktiv: Einfarbige Rechtecke in der Vorschau. Jeder Thumb wird nur auf Basis seiner Durchschnittsfarbe passend zum Bildbereich des Originals ausgewählt.
Verlauf-Modus aktiv: Dreifarbige Rechtecke in der Vorschau. Hier werden die Bildbereiche des Originalbildes auf gleich neun Farbwerte hin untersucht, ob es passende Thumbnails gibt, die diesen Bereich möglichst gut wiedergeben. Dieses Verfahren liefert meistens bessere Ergebnisse, insbesondere bei nur wenigen Mosaiken.
Zuletzt muss noch berücksichtigt werden, ob die Quader-Bilder einen Rand erhalten sollen.
Ist dem so, werden die Rechtecke der Vorschau mit einem schwarzen Rand versehen.
Ansonsten erhält der Rand die gleiche Farbe wie das Rechteck. Die in den Optionen
einstellbaren individuellen Randfarben werden in der Vorschau nicht berücksichtigt.
Rand aus: Rechtecke ohne Rand. Die einzelnen Kachelbilder treten bei diesem Verfahren nicht so deutlich in Erscheinung und verschmelzen besser mit dem Untergrund.
Rand an: Rechtecke mit Rand. Für eine natürlich anmutende Kachelung sollten die Mosaike z.B. oben und links helle Rändern bzw. unten und rechts dunkle Ränder besitzen. Dadurch wirken sie dreidimensionaler.
Haben wir letztendlich alle Pixel durchlaufen, verlassen wir die Prozedur.
Es sei denn, der Verwacklungsmodus ist aktiv. In diesem Fall kann es nämlich
passieren, dass die Rechtecke über das Ziel hinaus gemalt wurden - dem "inneren" Bild
innerhalb der Vorschau-PaintBox. Da das unschön aussieht, werden die Bereiche
ausserhalb des "inneren" Bildes mit silbergrauen Blöcken neu gezeichnet und so
die "Ausreisser-Rechtecke" überdeckt.
Ausreisser-Rechtecke I: Die Verwacklung lässt die Vorschau-Rechtecke ausserhalb des inneren Bildes wandern.
Ausreisser-Rechtecke II: Nach der Korrektur sieht es ordentlicher aus.
Liefert uns die Vorschau ein befriedigendes Ergebnis, kann mittels des "Start"-Buttons
die eigentliche Generierung des Ergebnisbildes, der Wallpaper, vorgenommen werden.
Dazu wird die Prozedur "op_picofpics" aufgerufen. Wie bereits erwähnt, gibt es hier
Parallelen zur Abarbeitung der Vorschau-Prozedur "op_mkprevbmp".
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273
00274
00275
00276
00277
00278
00279
00280
00281
00282
00283
00284
00285
00286
00287
//------------------------------------------------------------------------
//'kernel' of PicOfPics
//
//- transform original pic to pixelbmp with quad-dimension
//- find a quad with best color to every pixel in pixelbmp
//- paint founded quads on result-bmp
//
//------------------------------------------------------------------------
procedure op_picofpics;
var
zbmp,quadbmp,thquadbmp:tbitmap;
qvert,qhorz,qw,qh,
rr,px,py,x,y:integer;
qcol:tcolor;
fn:string;
rec:trect;
jpg:tjpegimage;
breakok:bool;
helligkeit,
verlauf:integer;
in_hellbereich:bool;
pba:pbytearray;
//paint founded quad to target bmp
procedure setquader(l,t:integer);
var
w,h:integer;
begin
w:=hauptf.op_qbreitese.value;h:=w;
//randomize quad dimension
l:=l+hauptf.verwackeln(w,hauptf.op_wxse.value);
w:=w+hauptf.verwackeln(w,hauptf.op_wwse.value);
t:=t+hauptf.verwackeln(h,hauptf.op_wyse.value);
h:=h+hauptf.verwackeln(h,hauptf.op_whse.value);
//copy quad to target
if hauptf.op_qqualicb.itemindex=0 then begin
//super-quality
quadbmp.assign(jpg);
//make a fine thumb
thquadbmp.width:=w;
thquadbmp.Height:=h;
op_optthumb(quadbmp,thquadbmp);
//copy thumb to target
zbmp.Canvas.Draw(l,t,thquadbmp);
end
else begin
//do only window-stretch-draw
rec:=rect(l,t,l+w,t+h);
zbmp.Canvas.StretchDraw(rec,jpg);
end;
if hauptf.op_qrandchb.checked then begin
//quader will have a border
zbmp.Canvas.Pen.Width:=1;
//border on top and left
zbmp.Canvas.pen.color:=
hauptf.op_qrandolsh.brush.color;
zbmp.canvas.MoveTo(l,t );
zbmp.canvas.lineto(l,t+h);
zbmp.canvas.MoveTo(l,t );
zbmp.canvas.lineto(l+w,t);
//border on bottom and right
zbmp.Canvas.pen.color:=
hauptf.op_qrandursh.brush.color;
zbmp.canvas.MoveTo(l+w-1,t+h-1);
zbmp.canvas.lineto(l, t+h-1);
zbmp.canvas.MoveTo(l+w-1,t+h-1);
zbmp.canvas.lineto(l+w-1,t );
end;
end;
begin
screen.Cursor:=crhourglass;
//real random-mode
randomize;
//original to pixelbmp with quad-dimension
ob_u.ob_mkpixelbmp(false);
//define some 'help'-images
jpg:=tjpegimage.Create;
zbmp:=tbitmap.create;
quadbmp:=tbitmap.create;
thquadbmp:=tbitmap.create;
thquadbmp.PixelFormat:=pf24bit;
//reset sum of color-errors
hauptf.op_erre.text:='0';
try
//set jpg-scale-quality
if hauptf.op_qqualicb.itemindex=0 then begin
//super-quality
jpg.scale:=jsFullSize;
end
else begin
jpg.scale:=tjpegscale(
hauptf.op_qqualicb.ItemIndex-1
);
end;
//quad-dimension
qhorz:=hauptf.op_qhorzse.value;
qvert:=strtoint(hauptf.op_qverte.text);
qw:=hauptf.op_qbreitese.value;
qh:=qw;
//init target bmp
zbmp.PixelFormat:=pf24bit;
zbmp.Width:=qhorz*qw;
zbmp.height:=qvert*qh;
if hauptf.op_backpicchb.checked then begin
//target-background is original
rec:=rect(0,0,zbmp.width,zbmp.height);
zbmp.Canvas.StretchDraw(rec,hauptf.ob_bmp);
end
else begin
//target-background is plane color
zbmp.canvas.brush.color:=hauptf.op_backsh.Brush.Color;
zbmp.canvas.pen.color:=hauptf.op_backsh.Brush.Color;
zbmp.Canvas.Brush.Style:=bssolid;
zbmp.Canvas.Rectangle(0,0,zbmp.width,zbmp.height);
end;
//bool for manually break
breakok:=false;
//Find best top-x quader for
//every pixel of pixelbmp
//adapt progressbar to 100 steps
hauptf.op_prgb.Max:=qvert;
rr:=qvert div 100;
if rr=0 then rr:=1;
//work on pixelmap in height
py:=0;
for y:=0 to qvert-1 do begin
//set progressbar
if y mod rr=0 then
hauptf.op_prgb.Position:=y;
application.processmessages;
//manually break?
if hauptf.op_mkpicb.caption<>'STOPP' then begin
breakok:=true;
break;
end;
//get line of colors in pixelbmp
pba:=hauptf.ob_pixelbmp.scanline[y];
//work on pixelmap in width
px:=0;
for x:=0 to qhorz-1 do begin
//random setting of quads
if random(100)<Hauptf.op_malwkse.value then begin
//get color you fant to replace thru quad-pic
if not hauptf.op_verlaufchb.checked then begin
//normal-mode: replace only one pixel
qcol:=hauptf.pba2col(pba,x);
end
else begin
//smooth-mode: replace group of 3 x 3 pixels
qcol:=qp_u.qp_middlecolor(
hauptf.ob_pixelbmp,
x*3,y*3,3,3
);
end;
//is gray-value of the color in chosen limits?
helligkeit:=hauptf.col2helligkeit(qcol);
in_hellbereich:=
(helligkeit>=hauptf.op_hellvonsb.position)and
(helligkeit<=hauptf.op_hellbissb.position);
if
((hauptf.op_hellmodecb.itemindex=0)and in_hellbereich)or
((hauptf.op_hellmodecb.itemindex=1)and not in_hellbereich)
then begin
//ok: find the best quad-pic in quad-grid
fn:=op_col2sgfn(qcol,x*3,y*3);
//load the founded quad
jpg.LoadFromFile(fn);
//copy quad to target bmp
setquader(px,py);
end;
end;
//increase target-position with quader-width
px:=px+qw;
end;
//increase target-position with quader-height
py:=py+qh;
//preview?
if hauptf.op_prevchb.Checked then begin
op_mkprevbmp(zbmp);
hauptf.op_prevpbpaint(nil);
end;
end;
//copy created target bmp to result
hauptf.eb_bmp.assign(zbmp);
//set hint-informations
hauptf.eb_pb.Hint:=
inttostr(zbmp.width)+
' x '+
inttostr(zbmp.Height)+
' Pixel';
hauptf.eb_pb.showhint:=true;
//reset scrollbars an size of result-image
hauptf.eb_hsb.Position:=0;
hauptf.eb_vsb.Position:=0;
eb_u.eb_orgsz;
//copy result to result-PaintBox
eb_u.eb_PaintBoxpaint;
//set merge-base to created target-pic
hauptf.eb_blendbmp.assign(zbmp);
//save target-pic (quad-pic)
hauptf.eb_quadbmp.assign(zbmp);
//save original for merging
hauptf.eb_orgbmp.Width:=zbmp.width;
hauptf.eb_orgbmp.height:=zbmp.height;
rec:=rect(0,0,zbmp.width,zbmp.height);
hauptf.eb_orgbmp.Canvas.StretchDraw(rec,hauptf.ob_bmp);
//opts of result to default
hauptf.eb_hellmodecb.itemindex:=0;
hauptf.eb_hellvonsb.Position:=0;
hauptf.eb_hellbissb.Position:=255;
hauptf.eb_blendorgrb.checked:=true;
hauptf.eb_blendmodecb.ItemIndex:=0;
hauptf.eb_blendsb.Position:=0;
//manually break?
if not breakok then begin
//no: adapt preview
op_updateprev;
//change to result-page of page control
hauptf.pctrl.ActivePage:=hauptf.eb_ts;
end;
//adapt sum of color-errors
verlauf:=1;
if hauptf.op_verlaufchb.checked then
verlauf:=9;
hauptf.op_erre.Text:=format(
'%f',
[
strtoint64(hauptf.op_erre.Text)/
(qhorz*qvert*verlauf)
]
);
finally
//clean up the shit
quadbmp.Free;
zbmp.Free;
jpg.Free;
hauptf.op_prgb.Position:=0;
screen.Cursor:=crdefault;
end;
end;
Im Gegensatz zur Vorschau (siehe dort) wird bei der Generierung des
Ergebnisbildes mit echten Zufallswerten gearbeitet. Dazu wird die
Delphi-Funktion "randomize" aufgerufen.
Anschliessend wird aus dem Originalbild wieder unsere Pixel-Bitmap
"ob_pixelbmp" konstruiert. Ist der Verlauf-Modus aktiv, so stehen
jeweils 3 x 3 Pixel der Pixel-Bitmap für ein zu findendes Quader-Bild.
Ist er inaktiv, dann wird jedes Pixel einzeln betrachtet.
Es werden dann einige Hilfsmalflächen initialisiert. Das TJPegImage
"jpg", welches die Quader-Bilder einladen wird, bekommt die gewünschte
Skalierungsqualität verpasst. Und die Zielbitmap "zbmp" - unser
Ergebnisbild - wird passend zur Anzahl und Breite/Höhe der Quader-Bilder
dimensioniert.
Je nach Einstellung in der Optionen-Page wird anschliessend die "zbmp" mit
einer einheitlichen Hintergrundfarbe versehen bzw. bekommt als Hintergrund
das Originalbild hineinkopiert.
Nun folgt eine Schleife über die gewünschte Anzahl vertikaler Quader.
Es ist hier zu beachten, dass diese Anzahl von der Höhe der Pixel-Bitmap
"ob_pixelbmp" abweichen kann, nämlich dann, wenn der Verlauf-Modus aktiv
ist (in diesem Fall ist die Pixel-Bitmap ja dreimal höher, siehe bei
"ob_mkpixelbmp").
Insofern ist es im Verlauf-Modus eigentlich auch sinnlos, sich per
"scanline"-Methode die Pixelfarben einer Zeile aus der Pixel-Bitmap zu
holen, wir wir es im nächsten Schritt machen. Das ist nur nötig für den
"Einfach-Modus". Da der Delphi-Compiler aber eine hässliche Warning
auswirft, wenn diese Zeile per "op_verlaufchb.checked"-Prüfung übergangen
wird, lesen wir sie trotzdem ein. Das geht so schnell, dass wir es
ignorieren können.
Die nächste Schleife durchläuft die gewünschte Anzahl horizontaler Quader.
Auch hier gilt, dass im Verlauf-Modus die Breite der Pixel-Bitmap dreimal
so gross ist.
Ähnlich wie bei der Vorschau-Prozedur wird anschliessend geprüft, ob uns die
Mal-Wahrscheinlichkeit grünes Licht für ein zu setzendes Quader-Bild
gibt oder nicht. Falls nicht, wird der aktuelle Quader ignoriert und mit dem
nächsten fortgefahren.
Soll der Quader gemalt werden, bestimmen wir zunächst die Mittel-Farbe des
Bereichs im Originalbild, der durch einen Quader zu ersetzen ist.
Im Falle des "Normal-Modus" ist dies einfach der Farbwert des aktuellen
Pixels der Pixel-Bitmap. Die bereits bekannte Funktion "pba2col" liefert
uns hierzu das passende Ergebnis.
Im "Verlauf-Modus" muss jedoch die Durchschnittsfarbe eines 3 x 3 Pixel-Blocks
aus der Pixel-Bitmap ermittelt werden. Auch dafür kennen wir bereits eine Funktion,
nämlich "qp_middlecolor" aus der Unit des Quader-Pools.
In beiden Fällen steht hinterher in "qcol" ein Farbwert, für den wir eventuell
ein passendes Quader-Bild finden müssen. Denn erst müssen wir noch prüfen, ob
dieser Farbwert überhaupt im gewünschten Helligkeitsbereich liegt. Ist dies nicht
der Fall, wird dieser Bereich des Originalbildes ignoriert.
Passt der Farbwert zu den Optionen, rufen wir als Nächstes die Funktion
"op_col2sgfn" auf, die uns aus unserem zuvor definierten Quader-Pool ein
geeignetes, sprich farbähnliches Quader-Bild heraus sucht. Diese Prozedur
sehen wir uns gleich noch etwas näher an.
Der gefundene Quader wird anschliessend mit der "loadfromfile"-Methode des
TJPegImages "jpg" eingeladen. Über die interne Prozedur "setquader" platzieren
wir dann den Quader an passender Stelle in die Ziel-Bitmap "zbmp" hinein.
Auch dazu gleich mehr.
So arbeiten wir nach und nach alle horizontalen Quader ab. Sind wir damit
fertig, prüfen wir, ob die Vorschau aktiv ist. Ist dies der Fall, so zeigen
wir die bisherige Ziel-Bitmap "zbmp" mittels der weiter oben beschriebenen
Vorschau-Prozedur "op_mkprevbmp" auf dem Bildschirm an.
Anschliessend nehmen wir uns die nächste Zeile des Originalbildes vor
und durchlaufen sie pixelweise, bis auch die alle abgearbeitet sind.
Am Schluss kopieren wir die fertige Ziel-Bitmap "zbmp" in die Ergebnis-Bitmap
"eb_bmp". Ausserdem setzen wir die Parameter der Ergebnisbild-Page auf ihre
Standards zurück. Und zuletzt berechnen wir noch die "Farbfehler-Summe", die
sich durch die Abweichungen der Mittel-Farbe der Quader-Bilder zur Mittel-Farbe
der Pixel-Bitmap ergeben hat.
Sehen wir uns jetzt noch an, was in der internen Prozedur "setquader" geschieht.
Ähnlich wie die Rechtecke in die Vorschau, so müssen auch die Quader-Bilder an
passender Stelle in die Ziel-Bitmap eingefügt werden.
Die korrekten Koordinaten werden an die Prozedur übergeben, müssen aber je nach
"Verwacklungsgrad" noch adaptiert werden. Dies geschieht in vertrauter Weise
mittels der "verwackeln"-Funktion.
Jetzt gilt es, das in "jpg" vorliegende Quader-Bild an die Quader-Dimension
in der Ziel-Bitmap anzupassen. Wurde in den Optionen für die Quader-Bilder
"Super"-Qualität gewählt, berechnet die Prozedur "op_optthumb" ein Thumbnail
passender Grösse. Diese Prozedur ist nicht von mir; ich fand sie vor ein paar
Jahren im Internet. Sie ist unglaublich kompliziert. Ich verstehe nicht einmal
näherungsweise, wie sie arbeitet. Ist mir aber auch egal. Sie liefert jedenfalls
sehr schöne verkleinerte Bilder zurück.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257
00258
00259
00260
00261
00262
00263
00264
00265
00266
00267
00268
00269
00270
00271
00272
00273
00274
00275
00276
00277
00278
00279
00280
00281
00282
00283
00284
00285
00286
00287
00288
00289
00290
00291
00292
00293
00294
00295
00296
00297
00298
00299
00300
00301
00302
00303
00304
00305
00306
00307
00308
00309
00310
00311
00312
00313
00314
00315
00316
00317
00318
00319
00320
00321
00322
00323
00324
00325
00326
00327
00328
00329
00330
00331
00332
00333
00334
00335
00336
00337
00338
//------------------------------------------------------------------
//Creates optimize Thumbs
//
//Stolen from Internet a view years ago.
//So I don't know the author of the Source.
//
//Difficult stuff! Good work, (wo)man!
//
//-------------------------------------------------------------------
procedure op_optthumb(Src,Dst:TBitmap);
type
// Contributor for a pixel
TContributor = record
pixel: integer; // Source pixel
weight: single; // Pixel weight
end;
TContributorList = array[0..0] of TContributor;
PContributorList = ^TContributorList;
// List of source pixels contributing to a destination pixel
TCList = record
n : integer;
p : PContributorList;
end;
TCListList = array[0..0] of TCList;
PCListList = ^TCListList;
TRGB = packed record
r, g, b : single;
end;
// Physical bitmap pixel
TColorRGB = packed record
r, g, b : BYTE;
end;
PColorRGB = ^TColorRGB;
// Physical bitmap scanline (row)
TRGBList = packed array[0..0] of TColorRGB;
PRGBList = ^TRGBList;
var
xscale, yscale : single; // Zoom scale factors
i, j, k : integer; // Loop variables
center : single; // Filter calculation variables
width, fscale, weight : single; // Filter calculation variables
left, right : integer; // Filter calculation variables
n,cc,ccmod : integer; // Pixel number
Work : TBitmap;
contrib : PCListList;
rgb : TRGB;
color : TColorRGB;
SourceLine ,
DestLine : PRGBList;
SourcePixel ,
DestPixel : PColorRGB;
Delta ,
DestDelta : integer;
SrcWidth ,
SrcHeight ,
DstWidth ,
DstHeight : integer;
fwidth:single;
ok:bool;
function Color2RGB(Color: TColor): TColorRGB;
begin
Result.r:=Color AND $000000FF;
Result.g:=(Color AND $0000FF00) SHR 8;
Result.b:=(Color AND $00FF0000) SHR 16;
end;
function RGB2Color(Color: TColorRGB): TColor;
begin
Result := Color.r OR (Color.g SHL 8) OR (Color.b SHL 16);
end;
function Lanczos3Filter(Value:Single):Single;
function SinC(Value:Single):Single;
begin
if Value<>0.0 then begin
Value:=Value*Pi;
Result:=sin(Value)/Value;
end
else begin
Result:=1.0;
end;
end;
begin
if Value<0.0 then Value:=-Value;
if Value<3.0 then Result:=SinC(Value)*SinC(Value/3.0)
else Result:=0.0;
end;
begin
ok:=false;
fwidth:=3.0;
DstWidth:=Dst.Width;
DstHeight:=Dst.Height;
SrcWidth:=Src.Width;
SrcHeight:=Src.Height;
if (SrcWidth<1)or(SrcHeight<1) then
raise Exception.Create('Source bitmap too small');
// Create intermediate image to hold horizontal zoom
Work:=TBitmap.Create;
try
Work.Height:=SrcHeight;
Work.Width:=DstWidth;
if SrcWidth=1 then xscale:=DstWidth/SrcWidth
else xscale:=(DstWidth-1)/(SrcWidth-1);
if SrcHeight=1 then yscale:=DstHeight/SrcHeight
else yscale:=(DstHeight-1)/(SrcHeight-1);
Src.PixelFormat:=pf24bit;
Dst.PixelFormat:=Src.PixelFormat;
Work.PixelFormat:=Src.PixelFormat;
// --------------------------------------------
// Pre-calculate filter contributions for a row
// -----------------------------------------------
GetMem(contrib,DstWidth*sizeof(TCList));
// Horizontal sub-sampling
// Scales from bigger to smaller width
if xscale<1.0 then begin
width:=fwidth/xscale;
fscale:=1.0/xscale;
for i := 0 to DstWidth-1 do begin
contrib^[i].n := 0;
GetMem(contrib^[i].p,trunc(width*2.0+1)*sizeof(TContributor));
center:=i/xscale;
// Original code:
// left := ceil(center - width);
// right := floor(center + width);
left := floor(center - width);
right := ceil(center + width);
for j := left to right do begin
weight := Lanczos3Filter((center - j) / fscale) / fscale;
if (weight = 0.0) then continue;
if (j < 0) then n := -j
else if (j >= SrcWidth) then n := SrcWidth - j + SrcWidth - 1
else n := j;
k := contrib^[i].n;
contrib^[i].n := contrib^[i].n + 1;
contrib^[i].p^[k].pixel := n;
contrib^[i].p^[k].weight := weight;
end;
end;
end
else begin
// Horizontal super-sampling
// Scales from smaller to bigger width
for i := 0 to DstWidth-1 do begin
contrib^[i].n := 0;
GetMem(contrib^[i].p, trunc(fwidth * 2.0 + 1) * sizeof(TContributor));
center := i / xscale;
// Original code:
// left := ceil(center - fwidth);
// right := floor(center + fwidth);
left := floor(center - fwidth);
right := ceil(center + fwidth);
for j := left to right do begin
weight := Lanczos3Filter(center - j);
if (weight = 0.0) then continue;
if (j < 0) then n := -j
else if (j >= SrcWidth) then n := SrcWidth - j + SrcWidth - 1
else n := j;
k := contrib^[i].n;
contrib^[i].n := contrib^[i].n + 1;
contrib^[i].p^[k].pixel := n;
contrib^[i].p^[k].weight := weight;
end;
end;
end;
// ----------------------------------------------------
// Apply filter to sample horizontally from Src to Work
// ----------------------------------------------------
for k:=0 to SrcHeight-1 do begin
SourceLine:=Src.ScanLine[k];
DestPixel:=Work.ScanLine[k];
for i := 0 to DstWidth-1 do begin
rgb.r := 0.0;
rgb.g := 0.0;
rgb.b := 0.0;
for j := 0 to contrib^[i].n-1 do begin
color := SourceLine^[contrib^[i].p^[j].pixel];
weight := contrib^[i].p^[j].weight;
if weight=0.0 then continue;
rgb.r := rgb.r + color.r * weight;
rgb.g := rgb.g + color.g * weight;
rgb.b := rgb.b + color.b * weight;
end;
if (rgb.r > 255.0) then color.r := 255
else if (rgb.r < 0.0) then color.r := 0
else color.r := round(rgb.r);
if (rgb.g > 255.0) then color.g := 255
else if (rgb.g < 0.0) then color.g := 0
else color.g := round(rgb.g);
if (rgb.b > 255.0) then color.b := 255
else if (rgb.b < 0.0) then color.b := 0
else color.b := round(rgb.b);
// Set new pixel value
DestPixel^ := color;
// Move on to next column
inc(DestPixel);
end;
end;
// Free the memory allocated for horizontal filter weights
for i:=0 to DstWidth-1 do FreeMem(contrib^[i].p);
FreeMem(contrib);
// -----------------------------------------------
// Pre-calculate filter contributions for a column
// -----------------------------------------------
GetMem(contrib, DstHeight* sizeof(TCList));
// Vertical sub-sampling
// Scales from bigger to smaller height
if yscale<1.0 then begin
width := fwidth / yscale;
fscale := 1.0 / yscale;
for i := 0 to DstHeight-1 do begin
contrib^[i].n := 0;
GetMem(contrib^[i].p, trunc(width * 2.0 + 1) * sizeof(TContributor));
center := i / yscale;
// Original code:
// left := ceil(center - width);
// right := floor(center + width);
left := floor(center - width);
right := ceil(center + width);
for j := left to right do begin
weight := Lanczos3Filter((center - j) / fscale) / fscale;
if (weight = 0.0) then continue;
if (j < 0) then n := -j
else if (j >= SrcHeight) then n := SrcHeight - j + SrcHeight - 1
else n := j;
k := contrib^[i].n;
contrib^[i].n := contrib^[i].n + 1;
contrib^[i].p^[k].pixel := n;
contrib^[i].p^[k].weight := weight;
end;
end
end
else begin
// Vertical super-sampling
// Scales from smaller to bigger height
for i := 0 to DstHeight-1 do begin
contrib^[i].n := 0;
GetMem(contrib^[i].p, trunc(fwidth * 2.0 + 1) * sizeof(TContributor));
center := i / yscale;
// Original code:
// left := ceil(center - fwidth);
// right := floor(center + fwidth);
left := floor(center - fwidth);
right := ceil(center + fwidth);
for j := left to right do begin
weight := Lanczos3Filter(center - j);
if (weight = 0.0) then continue;
if (j < 0) then n := -j
else if (j >= SrcHeight) then n := SrcHeight - j + SrcHeight - 1
else n := j;
k := contrib^[i].n;
contrib^[i].n := contrib^[i].n + 1;
contrib^[i].p^[k].pixel := n;
contrib^[i].p^[k].weight := weight;
end;
end;
end;
// --------------------------------------------------
// Apply filter to sample vertically from Work to Dst
// --------------------------------------------------
SourceLine := Work.ScanLine[0];
Delta := integer(Work.ScanLine[1]) - integer(SourceLine);
DestLine := Dst.ScanLine[0];
DestDelta := integer(Dst.ScanLine[1]) - integer(DestLine);
for k := 0 to DstWidth-1 do begin
DestPixel := pointer(DestLine);
for i := 0 to DstHeight-1 do begin
rgb.r := 0;
rgb.g := 0;
rgb.b := 0;
// weight := 0.0;
for j := 0 to contrib^[i].n-1 do begin
color := PColorRGB(integer(SourceLine)+contrib^[i].p^[j].pixel*Delta)^;
weight := contrib^[i].p^[j].weight;
if (weight = 0.0) then continue;
rgb.r := rgb.r + color.r * weight;
rgb.g := rgb.g + color.g * weight;
rgb.b := rgb.b + color.b * weight;
end;
if (rgb.r > 255.0) then color.r := 255
else if (rgb.r < 0.0) then color.r := 0
else color.r := round(rgb.r);
if (rgb.g > 255.0) then color.g := 255
else if (rgb.g < 0.0) then color.g := 0
else color.g := round(rgb.g);
if (rgb.b > 255.0) then color.b := 255
else if (rgb.b < 0.0) then color.b := 0
else color.b := round(rgb.b);
DestPixel^ := color;
inc(integer(DestPixel), DestDelta);
end;
Inc(SourceLine,1);
Inc(DestLine,1);
end;
// Free the memory allocated for vertical filter weights
for i := 0 to DstHeight-1 do FreeMem(contrib^[i].p);
FreeMem(contrib);
ok:=true;
finally
Work.Free;
if not ok then begin
application.messagebox(
'OptThumb misslungen',
'*** FEHLER **',
mb_ok
);
end;
end;
end;
Bei minder guter Qualität verwenden wir zum Anpassen des Quader-Bildes die
Canvas-Funktion "StretchDraw". Sie liefert ebenfalls brauchbare Ergebnisse,
arbeitet aber ungleich schneller als "op_optthumb".
Habe wir das Quader-Bild in die Ziel-Bitmap platziert, müssen wir eigentlich nur
noch prüfen, ob wir einen Rand einzeichnen müssen oder nicht. Falls ja, wird
die Pen-Color der Ziel-Bitmap entsprechend gesetzt und zuerst die obere und
linke, dann die untere und rechte Rand-Linie hineingemalt. That 's it!
Wir ermitteln in "op_picofpics" die Mittel-Farbe der Teile des Originalbildes,
die wir durch Quader-Bilder ersetzen wollen.
Dort wird die Funktion "op_col2sgfn" aufgerufen, die uns zu diesen Farben die am
besten passenden Quader-Bilder aus der Quader-StringGrid sucht. Die wollen wir uns
jetzt einmal ansehen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
//find best quad-file in quad-grid to a given color
function op_col2sgfn(col:tcolor;l,t:integer):string;
var
row,cmax,cdiff,r,rr,rmax:integer;
cdiffa:array of integer;
rowa:array of integer;
begin
setlength(cdiffa,hauptf.op_quaderbestse.value);
setlength(rowa,hauptf.op_quaderbestse.value);
row:=0;
for r:=1 to hauptf.qp_sg.RowCount-1 do begin
//get color difference of actually quad-entry
cdiff:=op_sgcoldiff(col,r,l,t);
//save best top-x of color diffs
if row<Hauptf.op_quaderbestse.value then begin
//top-x not full: save every cdiff
cdiffa[row]:=cdiff;
rowa[row]:=r;
inc(row);
end
else begin
//top-x full: save only better cdiffs
//find wirst hit in top-x
cmax:=-1;rmax:=0;
for rr:=0 to hauptf.op_quaderbestse.value-1 do begin
if cdiffa[rr]>cmax then begin
cmax:=cdiffa[rr];
rmax:=rr;
end;
end;
//new cdiff better then worst of top-x?
if cdiff<cmax then begin
//ok: replace enties
cdiffa[rmax]:=cdiff;
rowa[rmax]:=r;
end;
end;
//Top-1-Mode?
//Break, if there is cdiff-error=0
if
(hauptf.op_quaderbestse.value=1)and
(cdiff=0)
then break;
end;
//random access of top-x
r:=random(hauptf.op_quaderbestse.value);
//save sum of error of colors
hauptf.op_erre.Text:=inttostr(
strtoint64(hauptf.op_erre.Text)+
cdiffa[r]
);
//return founded quad-file
r:=rowa[r];
result:=
hauptf.qp_dlb.directory+'\'+
hauptf.qp_sg.cells[ord(_qp_fn),r];
end;
Um zu verhindern, das gleichfarbige Flächen stets mit dem gleichen,
weil allerbesten Quader-Bildern abgedeckt werden, kann man in den Optionen
angeben, dass aus den besten "x" Quader-Bildern zufällig eines gewählt
wird. "x" ist dabei über das TSpinEdit "op_quaderbestse" optional
einstellbar. Im ersten Schritt dimensionieren wir daher zwei Arrays
auf die passende Grösse, "cdiffa" und "rowa".
Danach durchlaufen wir alle Elemente der Quader-StringGrid. Die
Funktion "op_sgcoldiff" liefert uns die Differenz "cdiff" der
gesuchten, als Parameter übergebenen Farbe "col" zur Mittel-Farbe
bzw. zu den Verlaufsfarben des aktuellen Quader-Bildes. Diese
Funktion schauen wir uns nachher noch näher an.
Wir prüfen dann, ob bereits "x" Bilder gefunden wurden. Falls nicht,
können wir den Fehler "cdiff" an das Array "cdiffa" und die Quader-StringGrid-
Zeilennummer an das Array "rowa" einfach angehängt.
Haben wir bereits "x" Bilder gefunden, müssen wir zuerst prüfen, ob
der aktuelle Fehler "cdiff" kleiner ist als einer der im Array "cdiffa"
notierten Werte. Dazu ermitteln wir das grösste "cdiffa"-Element und
merken es uns in "cmax". Ist "cmax" grösser als "cdiff", dann ist das
aktuelle Quader-Bild ein besserer Treffer und wir tauschen die Array-Elemente
von "cdiffa" und "rowa" entsprechend aus.
Nach Beenden der Schleife haben wir die besten "x" Treffer gefunden
und wählen nun per Zufalls einen davon aus. Wir kalkulieren die
Farbfehler-Summe, die sich daraus ergibt und geben sie auf dem Bildschirm
aus. Zuletzt liefern wir den Namen der gewählten Quader-Bild-Datei an die
aufrufende Prozedur "op_picofpics" zurück.
Wie eben beschrieben, nutzen wir die Funktion "op_sgcoldiff" um den Farbfehler
zwischen einer Farbe "col" und den Farben (Mittelfarbe oder Verlaufsfarben) der
Zeile "r" in der Quader-StringGrid "qp_sg" zu ermitteln.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
//get diffenrence between col to col in quad-grid
function op_sgcoldiff(col:tcolor;r,l,t:integer):int64;
var
cdiff:int64;
qcol:tcolor;
c,x,y:integer;
pba:pbytearray;
begin
if not hauptf.op_verlaufchb.checked then begin
//check only middle color
qcol:=strtoint(
hauptf.qp_sg.cells[ord(_qp_farbe),r]
);
cdiff:=hauptf.getcoldiff(col,qcol);
end
else begin
//check all 3 x 3 'smooth-points'
cdiff:=0;
c:=ord(_qp_verlauf11);
for y:=t to t+2 do begin
pba:=hauptf.ob_pixelbmp.ScanLine[y];
for x:=l to l+2 do begin
col:=hauptf.pba2col(pba,x);
qcol:=strtoint(hauptf.qp_sg.cells[c,r]);
cdiff:=cdiff+hauptf.getcoldiff(col,qcol);
inc(c);
end;
end;
end;
result:=cdiff;
end;
Suchen wir nur nach der Differenz zur Mittel-Farbe, ist der Fall einfach,
denn die liegt in der Quader-StringGrid ja direkt als Einzelwert vor. Wir
holen uns den Wert, wandeln ihn in ein Integer und lassen den Rest von der
uns bereits bekannten Funktion "getcoldiff" berechnen.
Suchen wir dagegen nach der Differenz zu den Verlaufsfarben, müssen wir etwas
komplizierter vorgehen. Hier gilt es, 3 x 3 Pixel der Bitmap "op_pixelbmp" mit
den 9 Verlaufsfarben des Quader-StringGrid-Eintrags zu vergleichen und die
Farbdifferenzen in "cdiff" aufzusummieren.
Wie man vielleicht bemerken wird, dürfte "cdiff" im Falle der 9 Verlaufsfarben
deutlich grösser ausfallen als im Falle der einfachen Mittel-Farbe. Aus diesem
Grund sind die Farbfehlersummen, die wir auf dem Bildschirm ausgeben, auch
nicht mit einander zu vergleichen, wenn ein Foto-Mosaik einmal im Verlauf-Modus und
einmal im Normal-Modus generiert wird. Das ist etwas unschön, weshalb ganz
am Schluss - in "op_picofpics" - diese Fehlersumme noch etwas nachkorrigiert
wird. Dennoch, ein direkter Vergleich bleibt zweifelhaft.
Bild-Fehlersumme I: Deutlich kleinere Fehlersumme im Normal-Modus als in II.
Bild-Fehlersumme II: Die Fehlersumme ist im Verlauf-Modus viel grösser, obwohl die optischen Ergebnisse vergleichbar gut sind wie die aus I. Die Fehlersumme ist im Verlaufsmodus aus technischen Gründen immer grösser und sollte daher auch nicht mit den Fehlersummen des Normalmodus verglichen werden.
Wir haben einen Quader-Pool definiert, ein Originalbild ausgesucht, die Optionen
eingestellt und ein Ergebnisbild generieren lassen. Die letzte Page der PageControl
in PicOfPics, die wir uns jetzt ansehen werden, dient der Verwaltung und Modifizierung
des Ergebnisbildes.
Page des Ergebnisbildes: Auf dieser Registerseite kann das Ergebnisbild eingehend betrachtet werden. darüber hinaus lassen sich auch noch so einige Modifikationen im Zusammenhang mit der Einblendung des Originalbildes vornehmen. Das Endergebnis kann hier letztlich auch abgespeichert werden.
Ganz oben sehen wir die Buttons "Speichern" und "Entfernen". Der Delphi-Source für ihre
Funktionalität sieht folgendermassen aus:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
//check if result exists ---------------------
function eb_isempty:bool;
begin
result:=(hauptf.eb_bmp.width=1);
end;
//remove actally result----------------------
procedure eb_entfernen;
begin
hauptf.eb_fne.text:='';
hauptf.eb_bmp.width:=1;
hauptf.eb_bmp.height:=1;
hauptf.eb_bmp.modified:=false;
eb_PaintBoxpaint;
hauptf.setbuttons;
end;
//save result as jpg or bmp---------------------------
procedure eb_wrimg(fn:string);
var
jpg:tjpegimage;
begin
if extractfileext(fn)='' then begin
if hauptf.eb_picwrdlg.FilterIndex=1 then
fn:=fn+'.jpg'
else if hauptf.eb_picwrdlg.FilterIndex=2 then
fn:=fn+'.bmp';
end;
if fileexists(fn) then begin
if application.MessageBox(
pchar(
'Bild '+fn+' existiert bereits.'+_cr+
'Wirklich überschreiben?'
),
'*** FRAGE ***',
mb_yesno
)=id_no then exit;
end;
screen.Cursor:=crhourglass;
if lowercase(extractfileext(fn))='.bmp' then begin
try
hauptf.eb_bmp.SaveToFile(fn);
hauptf.eb_bmp.Modified:=false;
hauptf.eb_fne.Text:=fn;
except
application.messagebox(
pchar(
'Konnte Ergebnisbild '+
fn+
' nicht als Bitmap speichern!'
),
'*** FEHLER **',
mb_ok
);
end;
end
else begin
jpg:=tjpegimage.Create;
try
jpg.assign(hauptf.eb_bmp);
jpg.CompressionQuality:=70;
try
jpg.SaveToFile(fn);
hauptf.eb_bmp.Modified:=false;
hauptf.eb_fne.Text:=fn;
except
application.messagebox(
pchar(
'Konnte Ergebnisbild '+
fn+
' nicht als JPEG speichern!'
),
'*** FEHLER **',
mb_ok
);
end;
finally
jpg.Free;
end;
end;
screen.Cursor:=crdefault;
end;
Programmtechnisch interessant ist hier eigentlich nur, dass wir das Bild entweder
als JPG oder als Bitmap abspeichern können. Das wird entschieden durch
die Auswahl der File-Extension im Speicher-Dialog "eb_picwrdlg". Im Falle einer
Bitmap können wir die Ergebnis-Bitmap "eb_bmp" direkt abspeichern. Im Falle eines
JPGs konvertieren wir die Ergebnis-Bitmap über die "assign"-Methode von TJpegImage
in ein JPG, bevor wir es mit einer fixen Qualität von 70% abspeichern.
Um die Bildercollage besser analysieren zu können, lässt es sich mittels
diverser Zoom-Methoden vergrössern ("eb_zplus") und verkleinern ("eb_zminus").
Ebenso kann es mit einem Mausklick an den sichtbaren Bereich angepasst werden,
sodass es vollständig zu sehen ist ("eb_optsz"). Alternativ lässt es sich
mit einem Mausklick auf 100% Grösse setzen ("eb_orgsz").
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
//adapt result-bmp to PaintBox---------------------------
//=> get zoom-value
procedure eb_optsz;
var
z:double;
begin
if
(hauptf.eb_bmp.height/hauptf.eb_pb.Height)>
(hauptf.eb_bmp.width/hauptf.eb_pb.width)
then begin
z:=(hauptf.eb_pb.height/hauptf.eb_bmp.Height);
end
else begin
z:=(hauptf.eb_pb.width/hauptf.eb_bmp.width);
end;
hauptf.eb_ztb.position:=trunc(z*100);
eb_ztbChange;
end;
//set result to original-size-------------------
//=> zoom-value is 100%
procedure eb_orgsz;
begin
hauptf.eb_ztb.position:=100;
eb_ztbChange;
end;
//zoom into result------------------------
procedure eb_zplus(step:integer);
begin
hauptf.eb_ztb.position:=hauptf.eb_ztb.position+step;
eb_ztbChange;
end;
//zoom out of result-----------------------
procedure eb_zminus(step:integer);
begin
hauptf.eb_ztb.position:=hauptf.eb_ztb.position-step;
eb_ztbChange;
end;
//adapt zoom-trackbar to zoom-value-------------
procedure eb_ztbChange;
begin
hauptf.zl.Caption:=inttostr(hauptf.eb_ztb.position)+'%';
eb_PaintBoxpaint;
end;
Bei obigen Prozeduren wird im Wesentlichen nur auf verschiedene
Weisen der Zoom-Grad modifiziert und als Position in der TrackBar
"eb_ztb" gemerkt.
Originalgrösse bedeutet einen Zoom-Grad von 100. Bei optimaler Grösse
ergibt sich die Zoom-Grösse aus dem Verhältnis der PaintBox-Grösse
zur Grösse des Ergebnisbildes, welches in "eb_bmp" gespeichert
ist. Je nachdem, ob die Breite bzw. die Höhe überwiegt, wird dabei
entweder das Seiten- oder das Höhenverhältnis berücksichtigt.
Zoom I: Das Bild besitzt einen Zoom-Grad von 100 und damit Original-Grösse. Aus der Nähe betrachtet lassen sich meistens nur die Quader-Bilder gut erkennen, nicht aber das Gesamtbild.
Zoom II: Das Bild besitzt einen berechneten Zoom-Grad von 17% und damit in diesem Beispiel die optimale Grösse, um in der PaintBox komplett angezeigt zu werden. Es lässt sich nun gut abschätzen, ob das Originalbild gut getroffen wurde.
Die exakte Berechnung und Ausgabe des gezoomten Bildes erfolgt erst später,
nämlich in der "eb_mkpbbmp"- und "eb_PaintBoxPaint"-Prozedur. Die sehen wir uns
gleich an. Aber zuerst widmen wir uns den Scroll-Funktionen, die ebenfalls
Einfluss auf den sichtbaren Ausschnitt des Ergebnisbildes in der PaintBox
"eb_pb" haben.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
//set scrollbars in dependence of
//zoom-value and result dimension
procedure eb_setsbs;
var
w,h:integer;
sbv,sbh:tscrollbar;
z:double;
begin
z:=hauptf.eb_ztb.position/100;
try
h:=
round(hauptf.eb_bmp.height*z)-
hauptf.eb_pb.height;
if h<0 then h:=0;
w:=
round(hauptf.eb_bmp.width*z)-
hauptf.eb_pb.width;
if w<0 then w:=0;
sbv:=hauptf.eb_vsb;
sbh:=hauptf.eb_hsb;
sbv.Max:=h;
sbh.max:=w;
w:=sbv.Max div 10;if w<1 then w:=1;
h:=sbv.Max div 10;if h<1 then h:=1;
sbv.smallchange:=w;sbv.largechange:=w;
sbh.smallchange:=h;sbh.largechange:=h;
except
end;
end;
Egal, in welche Richtung gescrollt wird, es wird immer nur die
eine Prozedur "eb_setsbs" aufgerufen. Die Scroll-Werte stehen ja
automatisch in den Positionsattributen der ScrollBars "eb_vsb"
und "eb_hsb".
Modifiziert werden müssen aber - je nach Zoom-Grad und dadurch bedingte
Grösse der Mosaic-Collage - die Maxima der beiden ScrollBars sowie
auch ihre Scroll-Schritt-Grössen. Letztere werden so definiert, dass
mit 10 Schritten jeweils komplett von oben nach unten bzw. von links
nach rechts gescrollt werden kann.
Kennen wir den Zoom-Grad und die Scroll-Positionen, können wir
den Ausschnitt berechnen, der in der PaintBox "eb_pb" des Ergebnisbildes
angezeigt werden soll. Den Ausschnitt speichern wir dazu in "eb_pbbmp".
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
//transform result to PaintBox-bitmap
//look at zoom-value and scroll-position
procedure eb_mkpbbmp;
var
l,t,w,h,pw,ph:integer;
z:double;
begin
eb_setsbs;
z:=hauptf.eb_ztb.position/100;
pw:=hauptf.eb_pb.Width;
ph:=hauptf.eb_pb.height;
hauptf.eb_pbbmp.width:=pw;
hauptf.eb_pbbmp.height:=ph;
h:=round(hauptf.eb_bmp.Height*z);
if h<ph then t:=(ph-h)div 2 else t:=0;
if h>ph then h:=ph;
w:=round(hauptf.eb_bmp.width*z);
if w<pw then l:=(pw-w)div 2 else l:=0;
if w>pw then w:=pw;
hauptf.eb_pbbmp.canvas.pen.Color:=clsilver;
hauptf.eb_pbbmp.canvas.brush.Color:=clsilver;
hauptf.eb_pbbmp.canvas.Rectangle(0,0,pw,ph);
SetStretchBltMode(
hauptf.eb_pbbmp.canvas.handle,coloroncolor
);
try
stretchblt(
hauptf.eb_pbbmp.canvas.handle,
l,t,w,h,
hauptf.eb_bmp.canvas.handle,
trunc(hauptf.eb_hsb.position/z),
trunc(hauptf.eb_vsb.position/z),
round(w/z),round(h/z),
srccopy
);
except
end;
end;
//paint (part of) result in dependence
//of zoom-value and scroll-positions
procedure eb_PaintBoxPaint;
begin
try
eb_mkpbbmp;
bitblt(
hauptf.eb_pb.Canvas.Handle,
0,0,hauptf.eb_pb.width,hauptf.eb_pb.Height,
hauptf.eb_pbbmp.canvas.handle,0,0,
srccopy
);
except
end;
end;
Wir setzen in "eb_mkpbbmp" die Grösse der "eb_phbmp" gleich mit der
Grösse der PaintBox "eb_ph" des Ergebnisbildes. Durch ein
"OnResize"-Ereignis der Hauptform kann sich diese Grösse ja jederzeit
verändert haben.
Dann berechnen wir Breite und Höhe des "virtuellen" Bildes, welches sich
aus dem aktuellen Zoom-Grad ergibt, den wir in "z" gespeichert haben.
Ausserdem berechnen wir die Koordinaten Left "l" und Top "t" des "inneren"
Bildes, welches vorliegt, wenn der Ausschnitt des Ergebnisbildes kleiner
sein sollte als der PaintBox-Bereich.
Die PaintBox bekommt im nächsten Schritt einen silbergraue Hintergrundfarbe
verpasst. Anschliessend kopieren wir über die Windows-API-Funktion "StretchBlt"
das Ergebnisbild "eb_bmp" ab den Scroll-Positionen mit dem vorgegebenen Zoom-Grad
in die "eb_phbmp" hinein. Der vorherige Aufruf von "SetStretchBltMode" ist
hier übrigens nötig, da Windows standardmässig eine geringere "Stretch"-
Qualität als "coloroncolor" beim Canvas verwendet.
Löst die Ergebnis-PaintBox das Ereignis "OnPaint" aus, wird die Prozedur
"eb_PaintBoxPaint" ausgeführt. Die macht nun nichts anderes, als die
Bitmap "eb_phbmp" auf die eben beschriebene Weise zu generieren und dann
auf den Canvas der PaintBox "eb_ph" zu kopieren, wodurch sie für den Benutzer
sichtbar wird.
Wie wir gesehen haben, baut Pic-of-Pics das Originalbild aus einer bestimmten
Anzahl Quader-Bilder auf. Je mehr Quader-Bilder eingesetzt werden, desto
"schärfer" wird das Ergebnisbild. Diese "Schärfe" hat aber zweierlei
Nachteile: Erstens wird das Ergebnisbild unter Umständen sehr gross, was
lange Berechnungszeiten und viel Speicherbedarf mit sich bringt. Schwerer
wiegt aber wohl zweitens, dass nämlich irgendwann die Quader-Bilder gar nicht
mehr als Einzelbilder zu erkennen sind.
Anders herum bedeuten wenige Quader-Bilder logischerweise "Unschärfe", was im
ungünstigen Fall zur Folge hat, dass das Originalbild völlig "verschwimmt".
Quader-Schärfe I: Viele Quader-Bilder bringen zwar sehr schön Jessicas scharfe Kurven zur Geltung, lassen aber die Einzelbilder zu Beinahe-Punkten verkümmern.
Quader-Schärfe II: Wenige Quader-Bilder zeigen Jessica öfter, aber insgesamt undeutlicher.
Um dem eben geschilderten "Unschärfe"-Problem etwas entgegenzusetzten, verfügt PicOfPics
über eine Technik, mit der das Originalbild auf vielfältige Weise in das Ergebnisbild
eingeblendet werden kann.
Verblendung: Das Ausgangsbild wird in das Ergebnis-Quader-Bild einblendet.
Diese "Blend"-Funktionen schauen wir uns jetzt noch etwas näher an:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
//merge orgiginal or quad-result-bmp into result-image---------------------
procedure eb_blend;
var
bmp:tbitmap;
begin
if not hauptf.Visible then exit;
screen.Cursor:=crhourglass;
try
//copy blend-bmp to result-bmp
hauptf.eb_bmp.assign(hauptf.eb_blendbmp);
if hauptf.eb_blendorgrb.checked then begin
//merge original to result
bmp:=hauptf.eb_orgbmp;
end
else begin
//merge quader-result-pic to result
bmp:=hauptf.eb_quadbmp;
end;
//do merging, adapt eb_bmp
eb_blendbmps(
hauptf.eb_bmp,
bmp,
hauptf.eb_hellmodecb.itemindex,
hauptf.eb_hellvonsb.position,
hauptf.eb_hellbissb.position,
hauptf.eb_blendmodecb.itemindex,
hauptf.eb_blendsb.Position,
);
finally
screen.Cursor:=crdefault;
hauptf.eb_pbpaint(nil);
end;
end;
Die Prozedur "eb_blend" wird jedes Mal aufgerufen, wenn irgendetwas an den
Verblendungsoptionen geändert wird.
Zunächst kopieren wir das letzte Ergebnis-Blend-Bild "eb_blendbmp" in die
Ergebnis-Bitmap "eb_bmp". Die "eb_blendbmp" entspricht dabei so lange dem
originalem Ergebnis-Quader-Bild, bis der der Benutzer den Button "Übernehmen"
anklickt; dann wird das aktuelle Ergebnisbild "eb_bmp" zur neuen "eb_blendbmp",
und damit zur Basis möglicher weiterer Verblendungen.
Danach wird entschieden, welches Bild denn nun genau in die Blend-Bitmap einblendet
werden soll: Das Originalbild, vergrössert/verkleinert auf die Dimension des
Ergebnisbildes - das steht in "eb_orgbmp" -, oder das originale Ergebnis-Quader-Bild,
also das Bild, dass uns die Pic-of-Pics-Prozedur generiert hat, welches in "eb_quadbmp"
gespeichert wurde.
Die Verblendung selbst geschieht über die Prozedur "eb_blendbmps", wodurch die
Ergebnis-Bitmap "eb_bmp" den Einstellungen entsprechend modifiziert wird. Zu der
Prozedur kommen wir gleich noch.
Wie beschrieben kann das (durch die Verblendung modifizierte) Ergebnisbild durch
einen Klick auf "Übernahme" zur neuen Blend-Bitmap gemacht werden, indem die
folgende Funktion aufgerufen wird (aus Faulheit befindet sie sich in der Haupt-Unit,
gehört aber eigentlich in die Ergebnisbild-Unit):
00001
00002
00003
00004
00005
00006
00007
procedure Thauptf.eb_blendsvbClick(Sender: TObject);
begin
screen.Cursor:=crhourglass;
eb_blendbmp.Assign(hauptf.eb_bmp);
eb_blendsb.Position:=0;
screen.Cursor:=crdefault;
end;
Dadurch kann das Ergebnisbild "kaskadierend" modifiziert werden, indem abwechselnd
das Anfangsbild bzw. das Ergebnis-Quader-Bild als Quell-Bild in das Ergebnisbild
einblendet wird. Das erlaubt interessante Effekte. Und wenn das Quell-Bild zu
100% eingeblendet wird, können zudem alle vorherigen Modifikationen auch jederzeit
wieder übermalt werden.
Kaskade: I) Die Blend-Bitmap enthält das Ergebnis-Quader-Bild. II) In die Blend-Bitmap wird das Originalbild eingeblendet, das Ergebnis übernommen. III) In die neue Blend-Bitmap wird nun das Quader-Bild eingeblendet (im additiv-Modus).
Sehen wir uns jetzt die eigentlich Verblendungskernprozedur "eb_blendbmps" an, die die
Pixel der zwei gewählten Bitmaps miteinander vermischt.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
//merge blendbmp into basisbmp----------------------------
procedure eb_blendbmps(
basisbmp:tbitmap; //target and source
blendbmp:tbitmap; //source to merge in
hellmode:byte; //gray-value-mode
hellvon:integer; //gray-value start for hellmode
hellbis:integer; //gray-value end for hellmode
blendmode:byte; //merge-mode
transp:integer //strength of merging
);
var
basishelligkeit,blendhelligkeit,
basisgewicht,blendgewicht,
basisr,basisg,basisb,
blendr,blendg,blendb,
mischr,mischg,mischb,
x,y:integer;
basisba,blendba:pbytearray;
basiscol,blendcol:tcolor;
is_hellbereich:bool;
//convert int to byte------------
function i2b(i:integer):byte;
begin
if i>255 then i:=255;
if i<0 then i:=0;
result:=byte(i);
end;
begin
blendgewicht:=transp;
basisgewicht:=255-blendgewicht;
for y:=0 to basisbmp.height-1 do begin
//get line of color from source
blendba:=blendbmp.scanline[y];
//get line of colors from target
basisba:=basisbmp.scanline[y];
for x:=0 to basisbmp.width-1 do begin
//gray-value of source
blendr:=blendba[x*3+2];
blendg:=blendba[x*3+1];
blendb:=blendba[x*3+0];
blendcol:=rgb(blendr,blendg,blendb);
blendhelligkeit:=hauptf.col2helligkeit(blendcol);
//acceptable gray-value?
is_hellbereich:=
(blendhelligkeit>=hellvon)and
(blendhelligkeit<=hellbis);
if
((hellmode=0)and not is_hellbereich)or
((hellmode=1)and is_hellbereich)
then continue;
//gray-value of target
basisr:=basisba[x*3+2];
basisg:=basisba[x*3+1];
basisb:=basisba[x*3+0];
basiscol:=rgb(basisr,basisg,basisb);
basishelligkeit:=hauptf.col2helligkeit(basiscol);
//do blend-mode----------------------
mischr:=0;
mischg:=0;
mischb:=0;
if blendmode=0 then begin
//merge always
end
else if blendmode=1 then begin
//merge if source is darker
if blendhelligkeit>basishelligkeit then
continue;
end
else if blendmode=2 then begin
//merge if source is brighter
if blendhelligkeit<basishelligkeit then
continue;
end
else if blendmode=3 then begin
//merge additiv
mischr:=i2b((basisr*255+blendr*blendgewicht)div 255);
mischg:=i2b((basisg*255+blendg*blendgewicht)div 255);
mischb:=i2b((basisb*255+blendb*blendgewicht)div 255);
end
else if blendmode=4 then begin
//merge subractive
mischr:=i2b((basisr*255-blendr*blendgewicht)div 255);
mischg:=i2b((basisg*255-blendg*blendgewicht)div 255);
mischb:=i2b((basisb*255-blendb*blendgewicht)div 255);
end;
if blendmode<3 then begin
//calculate new mix color (merge-color)
mischr:=i2b((basisr*basisgewicht+blendr*blendgewicht)div 255);
mischg:=i2b((basisg*basisgewicht+blendg*blendgewicht)div 255);
mischb:=i2b((basisb*basisgewicht+blendb*blendgewicht)div 255);
end;
//bring new color to target
basisba[x*3+2]:=mischr;
basisba[x*3+1]:=mischg;
basisba[x*3+0]:=mischb;
end;
end;
end;
Für das Beispiel nehmen wir an, wir wollen die Original-Bitmap in das
aktuelle Ergebnisbild (das ist die Ergebnis-Quader-Bitmap der Pic-of-Pics-Prozedur)
einblenden. Dann entspricht der Parameter "basisbmp" der Ergebnis-Bitmap "eb_bmp"
und der Parameter "blendbmp" der Original-Bitmap "eb_orgbmp".
Wir durchlaufen die beiden Bitmaps zeilenweise und füllen jeweils ein
PByteArray ("basisba" und "blendba") mit den Pixelfarben.
In der inneren Schleife durchlaufen wir die Arrays "pixelweise". Den
Array-Index "x" müssen wir dabei jeweils mit 3 multiplizieren, da die
PByteArrays ja für jeden Pixel 3 Werte enthalten, nämlich für die Farbkanäle
blau, grün und rot (siehe weiter oben).
Zuerst betrachten wir das aktuelle Pixel der "blendbmp", also unseres
Originalbildes. Wir konvertieren die drei Farbkanalwerte von "blendba"
mittels "rgb"-Funktion zu einer Farbe. Diese Farbe rechnen wir anschliessend
mit "col2helligkeit" in einen Helligkeitswert um. Jetzt können wir prüfen,
ob wir uns im optional erlaubten Helligkeitsbereich befinden.
Ist das Pixel der Blend-Bitmap zu dunkel oder zu hell, wird es ignoriert.
Das heisst, das Pixel wird nicht in das Ergebnisbild eingeblendet. Was
wiederum heisst, an der Stelle bleibt die Basis-Bitmap komplett erhalten.
Verblendung Helligkeit I: Der komplette Helligkeitsbereich von 0 bis 255 wird beachtet. Die hellen Pixel des Basisbildes überdecken die Pixel der Quader-Bilder. Der Hintergrund verschwindet dadurch geradezu.
Verblendung Helligkeit II: Nur der Helligkeitsbereich von 0 bis 235 wird beachtet. Hellere Farben des Originalbildes werden entsprechend ignoriert. In diesem Bereich bleiben die Quader-Bilder zu 100% unverändert. Der Hintergrund ist deutlich sichtbar.
Verblendung Helligkeit III: Umkehrung der Beachtung des Helligkeitsbereichs (Helligkeit ignorieren). Nun bleiben überall dort die Quader erhalten, wo das Originalbild dunkler ist als 235.
Als Nächstes bestimmen wir die Helligkeit des aktuellen Pixels der "basisbmp",
in unserem Fall also der Ergebnis-Quader-Bitmap. Das Verfahren ist das Gleiche
wie bei der "blendbmp", nur dass diesmal auf das Pixel-Array "basisba" zugegriffen
wird.
Wir haben nun zwei Helligkeitswerte zur Verfügung, "blendhelligkeit" und "basishelligkeit".
Über diese Kriterien können wir das weitere Verhalten von PicOfPics bezüglich
des Blend-Modus steuern.
Im Falle des Blend-Modus "immer einblenden" müssen wir nichts weiter prüfen;
Basis- und Blend-Pixel werden unabhängig von ihrer Helligkeit zu einem neuen
Farbwert vermischt. Dazu gleich mehr.
Wurde jedoch der Blend-Modus "einblenden, wenn dunkler" gewählt, dann gilt:
Sollte die "blendhelligkeit" grösser als die "basishelligkeit" sein, sprich: heller,
dann ignorieren wir das aktuelle Pixel im Ziel, lassen es also unverändert.
Blend-Modus - Einblenden, wenn dunkler: Jessicas schwarzes Kleid überdeckt die Basis-Bitmap, ihr helles Gesicht dagegen kann sich nicht durchsetzen.
Genau umgekehrt verhält es sich beim Modus "einblenden, wenn heller": In diesem Fall
werden die Blend- und Basis-Pixel nur vermischt, wenn die "blendhelligkeit" grösser als
die "basishelligkeit" ist. Ansonsten wird das Pixel erneut unverändert gelassen. So
kann man helle Partien des Originals sehr schön aus den Quader-Bildern "herausscheinen"
lassen.
Blend-Modus: einblenden, wenn heller: Jessicas Gesichtsfarbe scheint überall durch. Ihre dunklen Haar- und Augenpartien lassen dagegen die Quader-Bilder unverändert.
Der Blend-Modus "additiv einblenden" bewirkt, dass die Farbwerte der Blend- und
Basis-Pixel aufaddiert, sie also quasi übereinander gestapelt werden. Dazu werden die
einzelnen Farbkanäle aufsummiert, und zwar so, dass die Basis-Farbwerte zu 100% gewichtet
werden, während die Blend-Farbwerte nur mit dem optionalen Grad der Verblendung berücksichtigt
werden. Dadurch werden die Farbwerte generell grösser, das Bild also insgesamt heller.
Blend-Modus additiv: Die Farbwerte der Quader und des Originals werden aufaddiert. Dadurch wird der ohnehin schon helle Hintergrund fast durchgehend weiss.
Ähnlich arbeitet der "Blend-Modus "subtraktiv einblenden", nur dass diesmal die Werte der
Farbkanäle voneinander abgezogen werden. Das Gesamtbild wird dadurch logischerweise
dunkler. Auch hier gilt, das die Farbwerte der Basis-Pixel vollständig beibehalten,
und die Blend-Pixel nur in Abhängigkeit vom Blend-Grad berücksichtigt werden.
Blend-Modus subtraktiv: Hier diente das Original als Basis- und die Quader als Blend-Bitmap. Jessicas schwarzes Kleid bleibt schwarz, ihr Gesicht wird dagegen um die Farbwerte der Quader verdunkelt.
Die Blend-Modi wurden abgearbeitet. Im Falle des additiven sowie des subtraktiven
Blend-Modus haben wir bereits die neuen Mischfarbwerte berechnet. Bei den anderen
Modi steht diese Berechnung noch aus.
Der Verblendungsgrad kann einen Wert von 0 bis 255 annehmen. 0 bedeutet, dass
jedes Basis-Pixel zu 100% und jedes Blend-Pixel zu 0% berücksichtigt werden soll.
Beim Blend-Grad 255 gilt genau das Gegenteil. Bei allen Werten dazwischen müssen
Mischfarben berechnet werden.
Die Gewichtung der Farbwerte von Basis- und Blend-Bitmap verläuft umgekehrt
proportional. Daher gilt: Das Blendgewicht entspricht dem Verblendungsgrad,
das Basisgewicht dem Wert "255-Blendgewicht". In Prozent ausgedrückt heisst
das z.B.: Die Blend-Bitmap soll zu 30% eingeblendet werden, also darf die
Basis-Bitmap nur zu 70% berücksichtigt werden.
Bei der Kalkulation der Mischfarbe werden daher die Rot-, Grün- und Blau-Anteile
des Basis-Pixels jeweils mit dem Wert von "basisgewicht" multipliziert. Entsprechend
wird mit dem Blend-Pixel verfahren, jetzt aber natürlich mit "blendgewicht"
gerechnet. Die beiden Werte werden je Farbkanal aufsummiert und anschliessend durch 255
geteilt, sodass sie sich wieder im erlaubten Bereich von 0 bis 255 bewegen. Als
Ergebnis erhalten wir die gewünschte Mischfarbe je Farbkanal.
In einem letzten Schritt muss jetzt nur noch das Basis-PByteArray mit den
eben berechneten Werten der Mischfarbkanäle gefüllt werden. Dadurch wird die
Zielbitmap an passender Stelle umgefärbt. Sind alle Pixel abgearbeitet,
liegt das neue Bild in "basisbmp" vor und kann in der aufrufenden Prozedur
"eb_blend" weiter behandelt werden (siehe weiter oben).
Damit hätten wir auch die Ergebnis-Page abgearbeitet. Bis auf eine Kleinigkeit.
Um sich beim Finetuning den ständigen Wechsel zwischen der Verblendung- und
Zoom-Page zu ersparen, habe ich nachträglich noch eine Maussteuerung für das
Scrollen und Zoomen des Ergebnisbildes eingebaut. Hier ist der Source dazu:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
//click on result-bmp-------------------------------
procedure eb_pbMouseDown(
Sender: TObject;
Button: TMouseButton;
Shift: TShiftState;
X, Y: Integer
);
begin
if button=mbmiddle then begin
//change size to optimum
eb_optsz;
exit;
end
else if button=mbright then begin
//change size to original
eb_orgsz;
exit;
end;
//save actually scroll-positions
hauptf.eb_hpos:=hauptf.eb_hsb.Position+x;
hauptf.eb_vpos:=hauptf.eb_vsb.Position+y;
hauptf.eb_scrollok:=true;
end;
//moving the mouse on result
procedure eb_pbMouseMove(
Sender: TObject;
Shift: TShiftState;
X,Y: Integer
);
begin
//left mousebutton down?
if not hauptf.eb_scrollok then exit;
//yep: scroll to new position
hauptf.eb_hsb.Position:=(hauptf.eb_hpos-x);
hauptf.eb_vsb.Position:=(hauptf.eb_vPos-y);
end;
//mouse button up over result
procedure eb_pbMouseUp(
Sender: TObject;
Button: TMouseButton;
Shift: TShiftState;
X, Y: Integer
);
begin
//disable scroll-mode
hauptf.eb_scrollok:=false;
end;
procedure Thauptf.FormMouseWheel(Sender: TObject; Shift: TShiftState;
WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
begin
if pctrl.ActivePage<>eb_ts then exit;
if wheeldelta<0 then eb_u.eb_zplus(1)
else eb_u.eb_zminus(1);
end;
Klickt der Anwender auf das Ergebnisbild, wird das "OnMouseDown"-Ereignis
ausgelöst, welches die Prozedur "eb_pbMouseDown" aufruft. Hier wird zunächst
geprüft, welcher Mausbutton gedrückt wurde. Ist es der mittlere, dann wird
das Bild auf optimale Grösse vergrössert/verkleinert. Ist es der rechte,
dann wird das Bild auf Original-Grösse gebracht. War es dagegen der
linke, dann merken wir uns die aktuelle Mausposition in den globalen Form-Variablen
"eb_hsv" und "eb_vsb". Ausserdem aktivieren wir den Scroll-Modus "eb_scrollok".
Verschiebt man die Maus über dem Ergebnisbild, dann wird "OnMouseMove" ausgelöst,
was die Prozedur "eb_pbMouseMove" aufruft. Hier wird zuerst geprüft, ob die
linke Maustaste überhaupt gedrückt ist. Das können wir über den booleschen Wert des
Scroll-Modus "eb_scrollok" feststellen. Die Abfrage ist nötig, da sonst jede
Mausänderung ein Scrollen des Ergebnisbildes bewirken würde. Ist der Scroll-Modus
aktiv, setzen wir die ScrollBars neu, in Abhängigkeit zur aktuellen Positionsänderung
seit dem letzten Klick mit der linken Maustaste.
Lässt der Anwender irgendwann die linke Maustaste wieder los, wird "OnMouseUp"
aufgerufen, was die Prozedur "eb_pbMouseUp" aufruft. Die macht dann nichts anderes,
als den Scroll-Modus (wieder) zu deaktivieren.
Das Zoomen des Ergebnisbildes mittels des mittleren Mausrades musste über das
Form-Ereignis "OnMouseWheel" gelöst werden, da die TPaintBox "eb_ph" nicht selbst
über dieses Ereignis verfügt. In der dadurch aufgerufenen Prozedur "FormMouseWheel"
wird daher zunächst festgestellt, ob wir uns derzeit auf der Page des Ergebnisbildes
befinden. Ist dies nicht der Fall, verlassen wir die Prozedur. Ansonsten wird - je
nachdem, ob das Mausrad hoch oder runter bewegt wurde - in das Ergebnisbild hinein-
("eb_zplus") oder heraus ("eb_zminus") gezoomt.
Bleibt noch zu sagen, dass eine ganze Reihe von Ideen, die ich ausprobiert
habe, nicht von Erfolg gekrönt waren.
So hatte ich ein Verfahren entwickelt, dass nicht - wie jetzt - zu jedem Pixel
der Pixel-Bitmap des Originalbildes ein Quader-Bild gesucht hat, sondern
umgedreht zu jedem Quader-Bild den oder die am besten passenden Pixel. Auf diese
Weise wurde sichergestellt, dass jedes Quader-Bild mindestens einmal verwendet
wurde (sofern die Anzahl Quader gross genug war). Zwar wurde dadurch das
Ergebnisbild abwechslungsreicher, das Originalbild ging aber regelmässig
derart "unter" in dem bunten Treiben, dass ich diesen Weg nicht weiter verfolgte.
Einige Experimente betrieb ich auch wegen der Form der Quader-Bilder im
Ergebnisbild. In Pic-of-Pics werden nämlich die Pool-Bilder durch die Bank weg
wie Quadrate eingesetzt (sofern keine "Verwacklung" von Breite und Höhe aktiv
ist). Es gilt ganz einfach: Ein Pixel, ein Quader.
Nun zeigt die Praxis aber, dass weitaus die meisten Bilder, die man so findet,
keineswegs quadratisch sondern rechteckig sind. Verwendet man diese, hat das
zwangsläufig zur Folge, dass die Quader-Bilder im Ergebnisbild verzerrt
auftauchen. Das ist unschön.
Eine Möglichkeit, dem zu begegnen, wäre, im Quader-Pool in einer eigenen
Spalte zu vermerken, ob das Quader-Bild nun hochkant-rechteckig, waagrecht-rechteckig
oder quadratisch ist. Den Abgleich der Quader-Bilder zur Pixel-Bitmap könnte man
dann folgendermassen erweitern: Ein Hochkant-Rechteck bestünde aus 2 x 3 Pixel,
ein Waagrecht-Rechteck aus 3 x 2 Pixel und ein Quadrat aus 2 x 2 Pixel. Wie man
sich jedoch leicht ausrechnen kann, ergibt sich daraus das Problem, dass die ganze
Sache am Schluss "aufgeht", d.h., alle Teile in diesem Puzzle so zusammenpassen,
dass es keine "Leerstellen" gibt.
Erfolgsversprechender war der umgedrehte Weg, den ich dann einschlug: Das Verfahren
"ein Pixel, ein Quader", hielt ich aufrecht, nur adaptierte ich jetzt das Quader-Bild,
"quadrierte" es gewissermassen. Lag es bereits als Quadrat vor, beliess ich es dabei.
War es ein Rechteck, dann "schnitt" ich mir einfach einen möglichst grossen quadratischen
Teil davon heraus. Diesen Ausschnitt zentrierte ich in Breite oder Höhe, je nachdem,
ob ein Waagrecht- oder Senkrecht-Rechteck vorlag. Das Ergebnis enttäuschte aber.
Zwar gab es nun keine Verzerrungen mehr, aber das Schnitt-Quadrat traf nur relativ
selten das eigentliche Motiv des Quader-Bildes. Gerade bei Senkrecht-Quadern wurden
häufig die Köpfe der dargestellten Personen abgeschnitten. Nein, nein, das war so nix.
Weiter spielte ich damit herum, die Histogramme von Quader-Pool und Originalbild
auf die einzelnen Farbkanäle hin zu erweitern. Statt also nur die verwendeten
Helligkeitsstufen anzuzeigen, konnte man sich optional auch alle Rot-, Grün-
oder Blau-Stufen präsentieren lassen. Der Informationsgewinn tendierte aber gegen
Null, ja, er verwirrte eher als dass er etwas nutzte.
Dann überlegte ich mir noch diverse "Umrahmungsmethoden", mit denen man die
Fotocollage verschönern könnte. Denn so ein Rahmen um das Bild macht manchmal schon
was was her. Aber hey, warum sich einen Kopf machen? Dafür gibt es schliesslich genügend
andere Programme (z.B. beherrscht das u.a. auch meine eigenes Grafik-Programm mit dem
etwas einfallslosen Titel "Graf").
Schliesslich startete ich Versuche, mehr Abwechslung in das Ergebnisbild zu bringen,
indem ich die Quader-Bilder gemäss ihrer Verlaufsfarben spiegelte, sodass sie sich
farblich noch besser zum Originalbild arrangieren liessen. Aber nicht jedes Motiv
macht gespiegelt Sinn, etwa wenn Schrift darauf zu lesen ist. Und woran sollte der
Computer dies erkennen?
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy.
Ist halt nichts 100%iges. But who cares?
Geniessen wir am am Schluss noch ein paar Beispiele aus der Welt von PicOfPics.
Einfach, damit wir sehen, wofür die ganze Schufterei gut war :-)
Beispiel I: Jessica Alba in spektraler Wiederholung.
|
Beispiel II: Schwarz-Weiss-Pixelblöcke in minderer Qualität.
|
Beispiel III: Schwarz-Weiss-Pixelblöcke in bester Qualität plus Verlauf-Modus. Dadurch erhält man ein extrem kontrastreiches Bild, wobei das Original gut zu erkennen bleibt.
|
Beispiel IV: Einheitlich gefärbte Blöcke als Quader-Bilder. Ähnlich wie Beispiel III, nur dass diesmal auch die originalen Farben einigermassen exakt wiedergegeben werden. Zusätzlich wurden die Bilder per Zufallsmodus noch etwas verteilt.
|
Beispiel V: Beste Quader-Bilder, Dominanz dunkler Farben des Originals. Das heisst, dass die hellen Flächen von Jessica Albas Oberkörper fast gar nicht in die Wallpaper eingeblendet wurden.
|
Beispiel VI: Demo: Flug in den Wolken.
|
Beispiel VII: Billardkugeln aus verschiedenen, unterschiedlich gefärbten Textur-Quader-Bildern.
|
Beispiel VIII: Kelly und Al Bundy bestehen aus einer TV-Bilder-Orgien. Die Treffer sind relativ grob, weil nur wenige Thumbnails gesetzt wurden. Dennoch erkennt man, dass die dunklen Mosaike gezielt auf den dunklen Flächen des Originals platziert wurden.
|
Sabrina Lloyd im Original
|
Sabrina Lloyd als Mosaik-Wallpaper: Im Gegensatz zu den vorherigen Beispielen wurden hier nur quadratische Bilder verwendet, wodurch die Thumbnails nicht verzerrt wiedergegeben werden.
|
Ein anderes Bild von Sabrina Lloyd im Original
|
Sabrina Lloyd mit schwarz-weiss Kacheln im Verlaufsmodus nachgebildet: Hier ist der Unterschied zum Original fast nur in der Vergrösserung zu erkennen.
|
Pic-of-Pics wurde in Delphi 7 programmiert. Im ZIP-File enthalten ist der
vollständige Source-Code, einige Originalbilder, mehrere Quader-Pools,
ein paar Ergebnisbilder sowie die EXE. Das ganze Paket, etwa 4 MB, ist
kostenlos und gibt es hier zum direkten Download:
PicOfPics.zip
Es wurde auf die Verwendung von Fremd-Komponenten verzichtet. Auch werden
keine speziellen DLLs benötigt. Der Source-Code lässt sich sicher leicht auf
andere Delphi-Versionen anpassen. Das ausführbare Programm ist mit 750 kB
im Vergleich zu manch anderem Grafik-Programm sehr klein. Ausserdem nimmt
es keine Änderungen an der Registry vor; alle Programm-Parameter werden über
eine INI-Datei im Arbeitsordner verwaltet.
Have fun!