C# - Bitmap közvetlen elérése és manipulálása

forráskód letöltése
Bitmap-ek pixelenkénti manipulálásakor igen érdekes effekteket, egyéb hatásokat érhetünk el. Ez viszont nagyon időigényes művelet lehet, főleg nagy képek kezelésekor. Most egy olyan módszert keresünk, mely ezt lehetővé teszi, méghozzá igen gyors eléréssel: közvetlen hozzáférést szerzünk a memóriában tárolt bitmap-hez.
A megvalósításhoz szükségünk lesz mutatók használatára, melyre a C# nyelvben csak úgy van lehetőségünk, hogy ha külön engedélyezzük a nem biztonságos kód blokkok (unsafe) készítését. Ehhez válasszuk a View - Solution Explorer menüpontot, majd itt az aktuális projekten kattintsunk jobb gombbal és válasszuk a Properties menüpontot. A megjelenő ablakból Configuration properties - Build elemet kijelölve a jobb oldali listából az Allow unsafe code blocks elemet állítsuk igazra. Ezt követően már használhatjuk a forráskódban az unsafe kulcsszót.
A kép jelenleg egy PictureBox-ban áll rendelkezésre.
Ez alapján készítünk egy Bitmap-et, melybe lemásoljuk a képet.
    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
    {
      Bitmap bmp = new Bitmap(pictureBox1.Image);
Ahhoz, hogy a képet közvetlenül elérhessük annak egészét, vagy egy részét zárolnunk kell, hogy más folyamat ne férhessen hozzá. Ehhez a Bitmap osztály LockBits függvényét kell meghívnunk. Első paraméterként azt a területet kell megadnunk Rectangle típusban, melyet kezelni szeretnénk. Ez a terület kerül zárolásra. Második property-ben azt határozhatjuk meg, hogy milyen módon szeretnénk hozzáférni. Ehhez az ImageLockMode felsorolt típus elemei közül választhatunk. Végül a PixelFormat szintén felsorolt típus elemeiből választhatunk egyet, melyben a zárolt terület pixeleinek típusát adja meg. A Format32bppArgb választásával a kép minden pixeléhez 32 bit tartozik, melyben 8-8 bit fogja tárolni az átlátszóság mértékét, a piros, a zöld és a kék színösszetevő értékét.
Visszatérési értékként kapunk egy BitmapData osztályt.
      BitmapData bd = bmp.LockBits(new Rectangle(0, 120, 256, 50), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
Készítünk egy belső Draw függvényt, mely elvégzi a szükséges manipulációt a képen. Ehhez két paraméter átadására lesz szükségünk: az első az a mutató, mely a zárolt terület első sorának első pixelére mutat. Ezt a Scan0 property adja IntPtr típusban. A második paramétert a BitmapData Stride property adja. Erre részletesebben a Draw függvény tárgyalásánál térünk ki.
      Draw(bd.Scan0, bd.Stride);  
A kirajzolás végeztével a zárolt területet fel kell oldanunk. Ezt az UnlockBits függvény teszi meg. Paraméterként a LockBits által visszaadott BitmapData osztályt kell megadnunk.
      bmp.UnlockBits(bd);
Végső lépésként már csak meg kell jelenítenünk a Form-on a kész bitmap-et.
      e.Graphics.DrawImage(bmp, 330, 20);
    }
Nézzük most a Draw függvény elkészítését. Mivel ebben mutatókat vagyunk kénytelenek használni, így a függvény deklarációja előtt használnunk kell az unsafe módosító kulcsszót.
    unsafe private void Draw(IntPtr ip, int Stride)
    {
A kapott IntPtr paramétert egy int-re mutató pointerre konvertáljuk. Valójában uint típust kellene használnunk, hiszen a 32 biten tárolt érték így értelmezendő, viszont ez esetben nem tudnánk használni a Color osztály függvényeit, melyekre szükségünk lesz és amelyek int típust várnak. Mivel a feldolgozás szempontjából lényegtelen, hogy int vagy uint típust használunk, így nyugodtan választhatjuk az int-et ez esetben.
      int* Pixels = (int*)ip;
      ...
Most szükségünk lesz két egymásba ágyazott ciklusra, mely végigmegy a szükséges terület minden egyes pixelén.
      for (int y=0; y<50; y++)
      {
        for (byte x=0; x<255; x++)
        {
Ha e terület első illetve utolsó soránál tartunk, akkor egyszerűen húzunk egy fehér vonalat, melyet természetesen pixelenként rajzolunk ki.
          if ((y==0) || (y==49))
          {
            c = Color.White;
A Pixels pointeren keresztül úgy érhetünk el egy x, y koordinátán lévő pixelt, hogy az x koordinátához hozzáadjuk a sorok számnak szorzatát a kapott Stride paraméter negyedével. Ennek oka a következő: a zárolt képterület pixelei a memóriában egymás után helyezkednek el. A kép terület bal felső pixele lesz a memória terület első 32 bitje. Ezután az első sor pixeleihez tartozó 32 bites egységek folyamatosan következnek ezen a memória területen. A kép második sorának pixeleihez tartozó 32 bitek pedig ezt követően, szintén folyamatosan kapnak helyet a memóriában. Ez így folytatódik az összes képsorra nézve. Így a két dimenziós kép a memóriában egy dimenzióban kerül tárolásra, ezért a Pixels „tömböt” is egy dimenziósként kezeljük és a megadott művelet alapján számítjuk ki, hogy az adott x, y koordináta melyik pozíció a memóriában. A Stride paraméterben pont azt kapjuk, hogy a kép egy sorához hány bájt szükséges a memóriában. Ez az információ nélkülözhetetlen a számítás elvégzéséhez. A néggyel való osztásra pedig azért van szükség, mert nekünk 32 bitenként kell „ugrálnunk”, hiszen ezt a PixelFormat-ot választottuk. Így ha például a Stride értéke 20, akkor tudhatjuk, hogy 5 pixel van egy sorban, vagyis 5 db 32 bites értéket találunk a memóriában.
A pixelnek így már egy tetszőleges szín értékét átadhatjuk. Ehhez egy 32 bites szám átadására van szükség, melyet a Color osztály ToArgb függvénye szolgáltat.
            Pixels[x + y * Stride/4] = c.ToArgb();
          }
          else
Ha nem az első, vagy utolsó sornál tartunk a képben, akkor a fehér vonal húzása helyett egy érdekes effektet készítünk. Ez változatlanul hagyja az adott pixel eredeti színét, de az Alpha csatornát az X ciklusváltozó függvényében állítja, vagyis a kép fokozatosan áttetsző lesz a háttérszínnel.
          {
            u = Pixels[x + y * Stride/4];
            c = Color.FromArgb(u);
            c = Color.FromArgb(x, c.R, c.G, c.B);
            Pixels[x + y * Stride/4] = c.ToArgb();