Entwickler-Ecke

Multimedia / Grafik - Große Karte laden - TBitmap?


lars2002 - So 17.01.10 18:16
Titel: Große Karte laden - TBitmap?
Hallo,

ich hab mal wieder ein Problem.
Und zwar hab ich ein Programm welches Sachen auf einem Stadtplan disponiert, der Plan selber ist ein normales Bimap als gif. Bisher waren das immer Kartenausschnitte mit etwa 800x600 pixel größe und daher einfach zu handhaben.

So und nun zum Problem ich möchte um nicht für jeden Einsatz ne neue Karte anlegen zu müssen mit Ortsmarken usw...nun gerne diesen Kartenausschnitt durch einen Stadtplan ersetzten. Das Problem der Stadtplan hat insgesamt eine größe von etwa 13.000 x 12.000 Pixeln geteilt in Kacheln zu je 1024x1024. Hat jemand nen Vorschlag wie man so große Grafiken am geschicktesten laden/anzeigen kann? Ein normales TBitmap ist damit ja leicht überfordert ;)

Bin für alle Vorschläge dankbar,

Gruß Lars


Gausi - So 17.01.10 18:32

Hallo und :welcome: in der Entwickler-Ecke!

Ein solches Bitmap hat im Speicher ein Größe von ca. 450MB. Sowas sollte man nicht komplett laden. Lade doch immer nur die Teile, die der User auch wirklich gerade sieht. Dann bleibt von dem 156 Megapixel-Bild ein etwas handlicherer Teil übrig.


lars2002 - So 17.01.10 18:38

Hi,

danke erstmal ;)

Ich komm rechnerisch auf knapp 200MB für das gesamte hat nur 256 Farben,
hmm wenn ich stückeweise lade hab ich immer noch ein gebiet von 4 Teilen also etwa 4100x4100 Pixel geladen auf was zeichne ich sowas denn am besten?


Gausi - So 17.01.10 18:52

Wenn du nicht auf Grafik-Engines umsteigen willst (z.B Fear2D [http://www.delphi-forum.de/viewtopic.php?t=62017]), dann würde ich nicht mit TImage arbeiten, sondern das ganze Zeug selbst in einer TPaintBox malen. Das benötigt zwar etwas Einarbeitungszeit, wenn man sowas noch nie gemacht hat, dürfte aber am Ende weniger Probleme machen als ein Rumgehampele mit TImage, Stretch und Proportional und dem ganzen Gedöns. :)

Um das TBitmap wirst du aber nicht ohne weiteres drumherum kommen.


Martok - So 17.01.10 18:58

Zeichnen per Canvas.Draw.

Du suchst dir also die Tiles aus, die grade gebraucht werden, berechnest, wo deren linke obere Ecke ist, und zeichnest die dort auf deine Darstellungsfläche.

Eventuell solltest du auch die Kacheln nochmal weiter zerlegen. 256*256 geht auch, und du hast dann weniger "verschwendeten" Platz.

@user profile iconGausi: ich würde aber trotzdem ein TImage nehmen. Da spart man sich den eigenen Backbuffer...


platzwart - So 17.01.10 19:00

Und dazu direkt ein Tip: Graphics32, google mal danach. Die sind unheimlich schnell und effizient.


lars2002 - So 17.01.10 19:14

Der Tip mit Graphics32 ist nicht schlecht da das ganze aber unter Lazarus/Linux laufen muss fällt das leider raus...


jaenicke - So 17.01.10 19:20

user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
@user profile iconGausi: ich würde aber trotzdem ein TImage nehmen. Da spart man sich den eigenen Backbuffer...
Das war bei mir aber auch deutlich langsamer als es selbst mit einer PaintBox zu machen. Und spätestens wenn man noch selbst gezeichnete Controls einblenden oder Schiebeeffekte realisieren will, ist man mit TImage ziemlich schlecht bedient. ;-)
(Abgesehen davon finde ich es in diesem Fall auch einfacher mit einer PaintBox, weil die Daten ja ohnehin schon im Hintergrund verwaltet werden müssen.)


Martok - So 17.01.10 19:37

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Das war bei mir aber auch deutlich langsamer als es selbst mit einer PaintBox zu machen. Und spätestens wenn man noch selbst gezeichnete Controls einblenden oder Schiebeeffekte realisieren will, ist man mit TImage ziemlich schlecht bedient. ;-)

Dann aber ganz ohne und direkt auf das Canvas des Formulars / eines Panels zeichnen. Da bringt die Paintbox nur noch eine sinnlose Schicht dazwischen.

Also: Backbuffer-TBitmap, das dann auf ein Canvas gezeichnet wird. Das macht man, damit das Ergebnis nicht flackert.
Der Backbuffer wird dann mit den eigentlichen, an der richtigen Stelle dargestellten Tiles gefüllt.


F34r0fTh3D4rk - So 17.01.10 20:13

user profile iconGausi hat folgendes geschrieben Zum zitierten Posting springen:
Wenn du nicht auf Grafik-Engines umsteigen willst (z.B Fear2D [http://www.delphi-forum.de/viewtopic.php?t=62017]), dann würde ich nicht mit TImage arbeiten, sondern das ganze Zeug selbst in einer TPaintBox malen. Das benötigt zwar etwas Einarbeitungszeit, wenn man sowas noch nie gemacht hat, dürfte aber am Ende weniger Probleme machen als ein Rumgehampele mit TImage, Stretch und Proportional und dem ganzen Gedöns. :)

Um das TBitmap wirst du aber nicht ohne weiteres drumherum kommen.

Ich bin mir nicht sicher, ob das mit Fear2D reibungslos klappen würde, aber ich habe mal vor einiger Zeit ein kleines Programm geschrieben, welches große Bilder in 512x512px große Stücke zerteilt und auf die Grafikkarte wuchtet.
Ich schau mal ob ich das hier noch hab, könnte vielleicht nützlich sein.

EDIT: Das ist, was ich auf die schnelle finden konnte (Die Unit ist in Freepascal geschrieben und sollte somit auch Platform-übergreifend nutzbar sein). Die Unit ist weder sonderlich optimiert, noch kommentiert, aber die prinzipielle Funktionsweise sollte ersichtlich sein. Die Zerteilung dient dazu, das ganze auch auf älteren Grafikkarten laufen lassen zu können.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
unit UBigBMP;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, graphics, dglOpenGL, glBitmap;
  
type
  TBitmapArray2 = array of array of TBitmap;
  TglBitmap2DArray2 = array of array of TglBitmap2D;
  TBigBMP = class
    destructor Destroy; override;
  private
    Textures: TglBitmap2DArray2;
    dList: TGLUInt;
    function SplitBitmapToArray(aBMP: TBitmap; maxw, maxh: integer; var rwidth, rheight: integer): TBitmapArray2;
  public
    function LoadFromFile(pFilename: string; maxwidth, maxheight: integer): boolean;
    procedure Render;
  end;

implementation

function NextPOT(a: integer): integer;
const
  arr_pot: array[0..7of integer = (81632641282565121024);
var
  i: integer;
begin
  for i := 0 to 7 do
    if (a <= arr_pot[i]) then
    begin
      result := arr_pot[i];
      exit;
    end;
  result := 1024;
end;

function ceil(a: single): integer;
begin
  if (frac(a) < 0.5then
    a += 0.5;
  result := round(a);
end;

function min(a, b: integer): integer;
begin
  if a < b then
    result := a else
      result := b;
end;

procedure FreeBitmapArray(aBMPArray: TBitmapArray2);
var
  i, j: integer;
begin
  for i := 0 to high(aBMPArray) do
    for j := 0 to high(aBMPArray[i]) do
      aBMPArray[i, j].Free;
end;

procedure FreeglBitmap2DArray(aBitmaps: TglBitmap2DArray2);
var
  i, j: integer;
begin
  for i := 0 to high(aBitmaps) do
    for j := 0 to high(aBitmaps[i]) do
      aBitmaps[i, j].Free;
end;

destructor TBigBMP.Destroy;
begin
  FreeGLBitmap2DArray(Textures);
  inherited Destroy;
end;

function TBigBMP.SplitBitmapToArray(aBMP: TBitmap; maxw, maxh: integer; var rwidth, rheight: integer): TBitmapArray2;
var
  w, h,
  mw, mh,
  cw, ch,
  i, j: integer;
  pw, ph: single;
begin
  w := aBMP.width;
  h := aBMP.height;
  mw := min(NextPot(w), NextPot(maxw));
  mh := min(NextPot(h), NextPot(maxh));
  rwidth := mw;
  rheight := mh;
  pw := w / mw;
  ph := h / mh;
  cw := ceil(pw);
  ch := ceil(ph);
  setlength(result, cw, ch);
  for i := 0 to (cw - 1do
    for j := 0 to (ch - 1do
    begin
      result[i, j] := TBitmap.Create;
      with result[i, j] do
      begin
        width := mw;
        height := mh;
        Canvas.CopyRect(Rect(00, mw, mh), ABMP.Canvas, Rect(i * mw, j * mh, (i + 1) * mw, (j + 1) * mh));
      end;
    end;
end;

function TBigBMP.LoadFromFile(pFilename: string; maxwidth, maxheight: integer): boolean;
var
  i, j,
  rwidth, rheight: integer;
  aStream: TStream;
  aBMP: TBitmap;
  aBMPArray: TBitmapArray2;
begin
  result := false;
  try
    if fileexists(pfilename) then
    begin
      aBMP := TBitmap.Create;
      try
        aBMP.loadfromfile(pfilename);
        aBMPArray := SplitBitmapToArray(aBMP, maxwidth, maxheight, rwidth, rheight);
      finally
        aBMP.Free;
      end;
      FreeGLBitmap2DArray(Textures);
      setlength(Textures, length(aBMPArray));
      aStream := TMemoryStream.Create;
      try
        for i := 0 to high(aBMPArray) do
        begin
          setlength(Textures[i], length(aBMPArray[i]));
          for j := 0 to high(aBMPArray[i]) do
          begin
            Textures[i, j] := TglBitmap2D.Create;
            aStream.Position := 0;
            aBMPArray[i, j].SaveToStream(aStream);
            with Textures[i, j] do
            begin
              aStream.Position := 0;
              LoadFromStream(aStream);
              GenTexture;
            end;
          end;
        end;
      finally
        aStream.Free;
        FreeBitmapArray(aBMPArray);
      end;
    end;
  except
    aBMP.Free;
    FreeBitmapArray(aBMPArray);
    exit;
  end;
  dList := glGenLists(1);
  glNewList(dList, GL_COMPILE);
    for i := 0 to high(Textures) do
      for j := 0 to high(Textures[i]) do
      begin
        Textures[i, j].Bind;
        glBegin(GL_QUADS);
          glTexCoord2f(01); glVertex3f( i      * rwidth, (j + 1) * rheight, 0);
          glTexCoord2f(11); glVertex3f((i + 1) * rwidth, (j + 1) * rheight, 0);
          glTexCoord2f(10); glVertex3f((i + 1) * rwidth,  j      * rheight, 0);
          glTexCoord2f(00); glVertex3f( i      * rwidth,  j      * rheight, 0);
        glEnd;
      end;
  glEndList;
  result := true;
end;

procedure TBigBMP.Render;
begin
  glCallList(dList);
end;

end.

Bisher konnte ich damit schon recht große Bilder anzeigen. Wenn es noch größer werden sollte, müsste man über weitere Optimierungen nachdenken. Der Nachteil ist natürlich, dass ein OpenGL-Renderkontext vorausgesetzt wird, dafür findet nahezu alles auf der Grafikkarte statt. dglOpenGL.pas und glBitmap.pas werden vorausgesetzt.

Verwendung:

Delphi-Quelltext
1:
2:
3:
4:
  // BigBMP
  BigBMP := TBigBMP.Create;
  if Opendialog1.execute then
    BigBMP.loadfromfile(opendialog1.filename, 512512);


Delphi-Quelltext
1:
  BigBMP.Render;                    


Ein kleines Beispielprogramm (nur EXE) habe ich hier mal angehängt. Beim Klick auf das Bild öffnet sich ein Dialog und man kann eine Bilddatei auswählen und anzeigen lassen. Mit den Pfeiltasten lässt sich das Bild bewegen und mit + und - kann man Zoomen.


jaenicke - So 17.01.10 20:17

user profile iconMartok hat folgendes geschrieben Zum zitierten Posting springen:
Dann aber ganz ohne und direkt auf das Canvas des Formulars / eines Panels zeichnen. Da bringt die Paintbox nur noch eine sinnlose Schicht dazwischen.
Ein Panel hat kein Canvas. ;-)

Und wenn ich direkt aufs Formular zeichne bekomme ich plötzlich mit exakt dem selben Code (bei meinem Periodensystem) Grafikfehler. Eigentlich sollte das aber schon auch funktionieren, allerdings halte ich es für wenig sinnvoll, weil man dann keine GUI mit weiteren Controls normal verwenden kann, auf denen dann irgendwo das Bild liegt.


F34r0fTh3D4rk - So 17.01.10 20:28

Hm ich lese gerade, dass das Bild schon zerlegt ist. Ich würde mir das Bild als ein Gitter vorstellen und immer nur die Bilder im Speicher haben, die gerade im sichtbaren Kartenausschnitt, bzw. in der näheren Umgebung liegen, um Popups zu vermeiden. Um das Ganze flüssig darzustellen, halte ich OpenGL für die beste Methode, es geht aber sicher auch einfacher.


Hidden - So 17.01.10 20:32

Hi :)

Auf Paintbox und Formular zu zeichnen, sollte doch, was Rechenzeit anbelangt, ungefähr aufs selbe hinauskommen. Die PBox buffert doch nicht nochmal, sondern leitet das Zeichen weiter? TImage buffert dagegen, das sollte auch deutlich langsamer sein, genau wie user profile iconjaenicke(?) schon geschrieben hat.

Beim Formular hast du aber im Unterschied zur PBox das Problem mit dem Repaint, das müsstest du dann selbst implementieren, oder aber die Grafik sieht sehr bemitleidenswert aus(Radieren mit 2. Formular ist :?).

mfG,

E: Das ganze könnte wahrscheinlich wesentlich ruckelfreier laufen, wenn die Ergebnisbilder zukünftiger Operationen wie Scrollen(hoch/runter/links/rechts), Zoom(in/out) bereits(in reduzierter Auflösung?) vorgeladen werden. Je nachdem, was der Speicher zulässt. Dann muss der Nutzer nicht extra auf das nächste Bild warten.
Man kann das sogar soweit treiben, die nächste Aktion vorauszusagen. Nach Scroll rechts folgt häufig Scroll Rechts, nach zoom out häufig ein weiteres zoom out.


lars2002 - So 17.01.10 20:38

Danke erstmal für die vielen Antworten da werde ich morgen den Tag mal mit basteln verbringen ;)

Das Bild ist zerlegt, ja, wobei ich es auch einmal zusammengefügt hab inzwischen um es ggf. nochmal in kleinere Teile zu zerlegen...


lars2002 - Di 19.01.10 10:50

So zuerst nochmal Danke für die vielen Tips :

Habs jetzt auch soweit hinbekommen das die benötigten Teile geladen (hab die Kacheln mal auf 256x256 reduziert) geladen werden in ein offscreen Bitmap gezeichnet und dann mit BitBtn auf eine PaintBox. Allerdings ruckelt es beim scrollen doch erheblich, ich nehm mal an durch das ständige Nachladen der Kacheln von der Festplatte, würde es einen Geschwindigkeitsvorteil bringen wenn man die nicht immer von der HDD lädt sondern z.B. in einer TJpeg Image Liste speichert? Das müsste doch auch vom RAM verbrauch noch akzeptabel sein, oder?

Gruß Lars


jaenicke - Di 19.01.10 11:20

Ich würde es so machen, dass ich die angrenzenden Bilder immer auch im Speicher halte. Bzw. als Jpegs auch ggf. mehr als Zwischenspeicher.

Die andere Frage ist wie du die Bilder auf der Festplatte hinterlegt hast. In einzelnen Dateien? Günstiger wäre es vermutlich zeilenweise und mit Zugriff via MMFs in größeren Dateien zu arbeiten. Dann musst du nämlich die Zwischenspeicherung weniger selbst verwalten, da du einfach den Bereich der MMF im Speicher angeben musst.


helgew - Di 19.01.10 12:44

Hi,

Ich würde das etwas einfacher gestalten und wie folgt vorgehen:
Da zum Verschieben stets eine zentrale Kachel und deren acht umliegende geladen sein müssen, genügt es, neun bitmaps als Puffer zu erzeugen. Das Nachladen der Daten wird in einen separaten Thread ausgelagert, ein flag zeigt an, ob die Kachel zu der entsprechenden Koordinate schon geladen ist, ansonsten zeigt man ein graues Rechteck an.
Der Trick kommt beim Nachladen. Entpacken von jpeg-Daten dauert zu lange und eine jpeg-Datei ist kompliziert aufgebaut. Aus einem Windows bitmap kann man aber sehr leicht durch Einlesen des headers (siehe http://en.wikipedia.org/wiki/BMP_file_format) zu beliebigen Bildzeilen und Pixeln springen. Bitmaps speichern die Bildinformation zeilenweise und von unten nach oben. Mit einfachen Skalierungskoeffizienten bekommt man dadurch die 1/n - Zoomstufen dazugeschenkt (1/2, 1/3, 1/4, ...). Der Puffer muss nur so groß sein, wie der überstrichene Bereich in einer Zeile, also maximal die Bildbreite. Wenn man diesen en bloc ließt, ist das ganze auch noch richtig schnell.
M.E. nach ist das ein idealer Kompromiss zwischen Geschwindigkeit und Speicherverbrauch.