10.Prednaska

Z Pascal
Prejsť na: navigácia, hľadanie
10. Prednáška

úlohy | cvičenie


Kopírujeme a modifikujeme obrázky

Na dnešnej prednáške sa naučíme pracovať s dvojrozmernými poľami, t.j. s takými poľami, ktorých prvkami sú opäť polia. Skôr ako zavedieme takúto štruktúru, naučíme sa pracovať s dvojrozmernými tabuľkami farebných bodov (pixelov), t.j. s obrázkami a bitmapami.

Vytvoríme nový projekt, v ktorom budú dve grafické plochy: Image1 a Image2. Do prvej prečítame z nejakého súboru bitmapu a túto potom prekopírujeme do druhej plochy. Pri kopírovaní ju môžeme rôzne meniť, prípadne meniť spôsoby kopírovania. S obrázkami budeme pracovať ako s dvojrozmernou tabuľkou, pričom prvým indexom bude x-ová súradnica (t.j. stĺpec tabuľky) a druhým indexom y-ová súradnica (t.j. riadok) farebného bodu (pixel). Bodky v grafickej ploche číslujeme od 0 do šírka-1, resp. 0 až výška-1.

Najprv pripravme nový projekt:

  • do formulára vložíme dve grafické plochy Image - upravíme im rozmery tak, aby boli rovnako veľké 256x256
  • do formulára vložíme niekoľko tlačidiel - postupne im budeme priraďovať rôzne funkcie (dvojkliknutím na príslušné tlačidlo)
  • formulár teraz môže vyzerať takto:
rozloženie komponentov vo formulári

Projekt uložíme do priečinka na disku a tiež sem do tohto istého priečinka prekopírujeme niekoľko obrázkov (súborov s príponou .BMP). Tieto súbory si môžete stiahnuť z Obrazky.zip. Postupne dvojklikneme na tlačidlá Button1, Button2 a Button3 a priradíme im akcie prečítanie obrázkov súborov do prvej grafickej plochy Image1, napr. takto

procedure TForm1.Button1Click(Sender: TObject);
begin
  Image1.Picture.LoadFromFile('macka.bmp');
end;
 
procedure TForm1.Button2Click(Sender: TObject);
begin
  Image1.Picture.LoadFromFile('tiger.bmp');
end;
 
procedure TForm1.Button3Click(Sender: TObject);
begin
  Image1.Picture.LoadFromFile('lazarus.bmp');
end;

Rovnakým spôsobom môžeme zobraziť aj obrázky iných formátov (napr. .gif, .jpg, .png, ...), ale takto prečítané súbory do grafickej plochy môžu spomaliť neskoršiu manipuláciu po pixeloch (ďalej uvádzané algoritmy môžu pracovať výrazne pomalšie).

Stavová premenná Pixels pre Canvas grafickej plochy nám umožňuje pristupovať k jednotlivým pixelom (farebným bodkám) obrázka: Pixels je ako dvojrozmerná tabuľka, v ktorej sú všetky políčka typu TColor (farba). Treba si zapamätať, že Pixels má prvý index x-ovú súradnicu (stĺpec) a druhý index y-ovú súradnicu (riadok) - pri dvojrozmerných poliach to bude väčšinou naopak. Postupne budeme kopírovať obrázok z Image1 do Image2 po riadkoch:

procedure TForm1.Button4Click(Sender: TObject);
var
  X, Y: Integer;
begin
  for Y := 0 to Image1.Height - 1 do
  begin
    for X := 0 to Image1.Width - 1 do
      Image2.Canvas.Pixels[X, Y] := Image1.Canvas.Pixels[X, Y];
    Repaint;
  end;
end;

Repaint prekreslí druhú grafickú plochu po prekopírovaní každého riadka, aby sa postupne zobrazovali kopírované riadky (inak by sa zobrazila až záverečná zmena). Ďalší variant programu sa hrá s farbami cez ich položky RGB:

procedure TForm1.Button5Click(Sender: TObject);
var
  X, Y: Integer;
  Farba: TColor;
  R, G, B: Byte;
begin
  for Y := 0 to Image1.Height - 1 do
  begin
    for X := 0 to Image1.Width - 1 do
    begin
      Farba := Image1.Canvas.Pixels[X, Y];
      R := Red(Farba);
      G := Green(Farba);
      B := Blue(Farba);
      // R := (R + G + B) div 3;
      Image2.Canvas.Pixels[X, Y] := RGBToColor(R, G, B);    // RGBToColor(R, R, R); alebo RGBToColor(R, 0, 0);
    end;
    Repaint;
  end;
end;

Funkcia Red vráti červenú zložku farby, podobne Green a Blue vrátia zelenú a modrú zložku. Konkrétne tento príklad neurobí nič iné, ako skopíruje obrázok z Image1 do Image2. Lenže tu môžete elegantne experimentovať s farbami, napr. niektorú zložku vynulovať, alebo vydeliť dvomi, alebo hoci všetky tri spriemerovať (vznikne čiernobiely obrázok).

Pripravme procedúra TForm1.Image2MouseMove, do ktorej napíšeme, čo všetko chceme urobiť pri každom pohnutí myši nad plochou Image2:

procedure TForm1.Image2MouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
begin
  if Shift = [ssLeft] then
    Image2.Canvas.Pixels[X, Y] := Image1.Canvas.Pixels[X, Y];
end;

Aby sa pri pohybe neobjavovala len malá bodka ale nejaké okolie bodu, môžeme zapísať:

procedure TForm1.Image2MouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
const
  D = 3;
var
  XX, YY: Integer;
begin
  if Shift = [ssLeft] then
    for XX := X - D to X + D do
      for YY := Y - D to Y + D do
        Image2.Canvas.Pixels[XX, YY] := Image1.Canvas.Pixels[XX, YY];
end;

Ďalšie námety:

  • pri kopírovaní pixelov môžete posúvať začiatok každého riadka o nejakú vypočítanú hodnotu, napr. pomocou funkcie Sin
  • obrázky môžete preklápať podľa rôznych osí
  • obrázok sa dá zväčšovať/zmenšovať, resp. kopírovať viackrát na rôzne pozície; v nejakej časti zahusťovať a pod.
  • je zaujímavé pohrať sa s farbami - robiť rôzne jasy, stmavieť obrázok ku okrajom plochy, zvýrazňovať len nejakú farebnú zložku (napr. "dozelena")
  • z obrázka prekopírovať len tie pixely, ktoré sú od nejakého bodu (napr. (150,150)) nie ďalej ako napr. 100 -- ostatné pixely zafarbiť na bielo
  • otočiť obrázok Image1 o 90 stupňov
  • otočenie Image1 o 90 stupňov na svojom mieste tak, že sa pritom nepoužijete Image2 (ani iný TImage) a ani iná dátová štruktúra (napr. pomocné pole)


Viacrozmerné polia

Zovšeobecníme polia: prvkami poľa môže byť opäť pole -- ak je prvkom jednorozmerné pole, tak výsledný typ je dvojrozmerné pole. Ak máme dvojrozmerné pole A, potom A[4][2] označuje druhý prvok v štvrtom poli - väčšinou si dvojrozmerné pole predstavujeme ako tabuľku, v ktorej každý prvok leží v nejakom riadku a nejakom stĺpci - skrátene to môžeme zapísať A[4, 2].

Príklad: Je dané dvojrozmerné pole znakov, ktoré je už zaplnené nejakým textom. Napíšte program, ktorý presunie prvý riadok do druhého, druhý do tretieho, atď., až posledný do prvého (napr. motivácia: rolovanie obrázka). Najprv nie pekné riešenie:

var
  Pole: array [1..10] of array [1..25] of Char;
  Riadok: array [1..25] of Char;
  I, J: Integer;
begin
  ... // pole je už zaplnené
  for I := 1 to 25 do     // odložíme posledný riadok do pomocného poľa
    Riadok[I] := Pole[10, I];
  for I := 9 downto 1 do  // postupne kopírujeme všetky riadky
    for J := 1 to 25 do
      Pole[I + 1, J] := pole[I, J];
  for I := 1 to 25 do     // presunieme pomocné pole do prvého riadka
    Pole[1, I] := Riadok[I];
  ...

A teraz zapíšeme to isté, ale využijeme to, že ak sú dve polia zadeklarované identicky, môžeme ich kopírovať obyčajným priradením:

type
  TRiadok = array [1..25] of Char;
  TPole = array [1..10] of TRiadok;
var
  Pole: TPole;
  Riadok: TRiadok;
  I: Integer;
begin
  ...
  Riadok := Pole[10];
  for I := 9 downto 1 do
    Pole[I + 1] := Pole[I];
  Pole[1] := Riadok;
  ...

Nasledujúci príklad ukazuje spôsob prezerania prvkov dvojrozmerného poľa (zisťujeme, či matica NxN je symetrická). Najprv ukážeme nie najlepší spôsob riešenia:

const
  N = 100;
type
  TMatica = array [1..N, 1..N] of Integer;
 
function Symetricka(const M: TMatica): Boolean;
var
  I, J: Integer;
begin
  Result := True;
  for I := 2 to N do
    for J := 1 to I - 1 do
      if M[I, J] <> M[J, I] then
      begin
        Result := False;
        Break;
      end;
end;

Toto riešenie sa snaží použiť príkaz Break, ktorý spôsobí vyskočenie z najvnútornejšieho cyklu. To je ale nedostatočné, lebo aj tak sa budú kontrolovať ešte všetky nasledujúce riadky poľa. Keďže po skončení cyklu táto funkcia končí, v tomto prípade môžeme použiť príkaz na vyskočenie z podprogramu - Exit. Pozrite sa ešte na riešenie, ktoré je z programátorského hľadiska správnejšie - ak potrebujeme vyskakovať z viacerých cyklov, nepoužijeme for-cyklus ale while-cyklus:

function Symetricka(const M: TMatica): Boolean;
var
  I, J: Integer;
begin
  Result := True;
  I := 2
  while Result and (I <= N) do
  begin
    J := 1;
    while Result and (J <= I - 1) do
    begin
      if M[I, J] <> M[J, I] then
        Result := False;
      Inc(J);
    end;
    Inc(I);
  end;
end;


Hra LIFE

Ukážeme pre programátorov veľmi známu tému hru Life, v ktorej sa simuluje život organizmov vo svete na štvorčekovom papieri. Na každom políčku sa môže nachádzať jedna bunka, prípadne sa tu nová bunka môže narodiť, resp. aj umrieť. Vývoj, keď zo starej generácie buniek vznikne nová generácia, funguje podľa týchto pravidiel:

  • každé políčko má 8 susedných políčok (aj po uhlopriečke),
  • ak má živá bunka práve 2 alebo 3 susedov (na susedných políčkach sú živé bunky), tak prežije aj do ďalšej generácie,
  • ak má živá bunka 1 alebo žiadneho suseda, prípadne, ak ich má viac ako 3, tak do ďalšej generácie sa nedostáva - umiera,
  • ak má prázdne políčko práve troch živých susedov, narodí sa tu nová bunka.

Na simuláciu života použijeme dvojrozmerné pole logických hodnôt: True bude označovať políčko s bunkou, False bude prázdne políčko. Toto pole zadeklarujeme ako globálnu premennú Pole, aby sme naňho videli z viacerých podprogramov. Pomocná procedúra Kresli vykreslí toto pole do grafickej plochy, pričom plátno (Canvas) tejto plochy dostane ako parameter - robíme to preto, aby sme videli akým spôsobom sa pracuje s takýmto parametrom. V budúcnosti sa nám to môže zísť. Funkcia PocetSusedov pre dané políčko zistí, koľko má susedov so živými bunkami - treba si dávať pozor, aby sme pritom pole neindexovali mimo rozsah indexov <1, N> a aby sme do toho nezapočítali aj samotné zisťované políčko.

Ako už vieme, udalosť FormCreate sa automaticky spúšťa pri štarte projektu a preto ju využívame ako inicializáciu premenných, resp. spustenie nejakých akcií. V našom prípade tu vygenerujeme pole s náhodnými hodnotami (do každého políčka dáme True alebo False podľa výsledku funkcie Random - všimnite komentár v tomto riadku, ktorý dáva rovnaký výsledok) a vykreslíme ho pomocou procedúry Kresli. Zatlačenie tlačidla Button1 vygeneruje v štvorčekovej ploche nasledujúcu generáciu života. Tu si všimnite priradenie Pole := PomPole; ktoré prekopíruje celé dvojrozmerné pole PomPole (nová generácia) do poľa Pole.

Môžeme pridať udalosť onMouseDown: táto udalosť vzniká pri klikaní do plochy ľavým tlačidlom myši, pričom vo formálnych parametroch X a Y získavame informáciu o súradniciach kliknutého miesta. Kliknutím do plochy budeme môcť zmeniť obsah ľubovoľného políčka.

const
  N = 50;        // veľkosť štvorčekovej plochy
  Vel = 8;       // veľkosť políčka
 
type
  TPole = array [1..N, 1..N] of Boolean;
 
var
  Pole: TPole;
 
procedure Kresli(C: TCanvas);
const
  F: array [Boolean] of TColor = (clWhite, clBlack);
var
  Riad, Stlp: Integer;
begin
  for Riad := 1 to N do
    for Stlp := 1 to N do
    begin
      C.Brush.Color := F[Pole[Riad, Stlp]];
      C.Rectangle(Stlp * Vel, Riad * Vel, Stlp * Vel + Vel + 1, Riad * Vel + Vel + 1);
    end;
end;
 
procedure TForm1.FormCreate(Sender: TObject);
var
  Riad, Stlp: Integer;
begin
  Randomize;
  Image1.Canvas.FillRect(Image1.ClientRect);
  Image1.Canvas.Pen.Color := clSilver;
  for Riad := 1 to N do
    for Stlp := 1 to N do
      Pole[Riad, Stlp] := Random(5) = 0;
  Kresli(Image1.Canvas);
end;
 
function PocetSusedov(Riad0, Stlp0: Integer): Integer;
var
  Riad, Stlp: Integer;
begin
  if Pole[Riad0, Stlp0] then
    Result := -1
  else
    Result := 0;
  for Stlp := Stlp0 - 1 to Stlp0 + 1 do
    for Riad := Riad0 - 1 to Riad0 + 1 do
      if (Stlp >= 1) and (Stlp <= N) and (Riad >= 1) and (Riad <= N) and Pole[Riad, Stlp] then
        Inc(Result);
end;
 
procedure TForm1.Button1Click(Sender: TObject);    // vygeneruj nasledujúcu generáciu
var
  Riad, Stlp, Pocet: Integer;
  PomPole: TPole;
begin
  for Riad := 1 to N do
    for Stlp := 1 to N do
    begin
      Pocet := PocetSusedov(Riad, Stlp);
      PomPole[Riad, Stlp] := (Pocet = 3) or (Pocet = 2) and Pole[Riad, Stlp];
    end;
  Pole := PomPole;
  Kresli(Image1.Canvas);
end;
 
procedure TForm1.Button2Click(Sender: TObject);    // vyčisti plochu
var
  Riad, Stlp: Integer;
begin
  for Riad := 1 to N do
    for Stlp := 1 to N do
      Pole[Riad, Stlp] := False;
  Kresli(Image1.Canvas);
end;
 
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
const
  F: array [Boolean] of TColor = (clWhite, clBlack);
var
  Riad, Stlp: Integer;
begin
  Stlp := X div Vel;
  Riad := Y div Vel;
  if (Stlp >= 1) and (Stlp <= N) and (Riad >= 1) and (Riad <= N) then
  begin
    Pole[Riad, Stlp] := not Pole[Riad, Stlp];
    Image1.Canvas.Brush.Color := F[Pole[Riad, Stlp]];
    Image1.Canvas.Rectangle(Stlp * Vel, Riad * Vel, Stlp * Vel + Vel + 1, Riad * Vel + Vel + 1);
  end;
end;


späť | ďalej