Laden...

Bitmap-Manipulation (MemBitmap)

Erstellt von zommi vor 15 Jahren Letzter Beitrag vor 12 Jahren 21.681 Views
zommi Themenstarter:in
1.361 Beiträge seit 2007
vor 15 Jahren
Bitmap-Manipulation (MemBitmap)

Beschreibung:
Eine (weitere) Bitmap-Klasse zur schnellen Manipulation von Bildern.

Einleitung:
Seit längerem arbeite ich unter C++ im Bereich Bildverarbeitung. Dass da meistens alles "unsafe" abläuft, wird wahrscheinlich den meisten klar sein.
Dass sich aber andererseite managed/safe/C#-Code und Geschwindigkeit nicht immer ausschließt, hat bereits GetPixel und SetPixel um Längen geschlagen. 800 mal schneller gezeigt.
Und da ich momentan versuche vieles auf das viel schönere 😁 C# umzustellen, stell ich hier mal einen Teil - genauer gesagt die MemBitmap-Klasse online.

Da die herangehensweise eine ganz andere ist als bei der RoBitmap/FastBitmap-Klasse, ist dies mehr als spezielle Ergänzung zu sehen. Zumal meine Herangehensweise auch nicht mit dem CompactFramework funktioniert.

Implementierung:
Soweit so gut: Nun zu den Details:
Der "Trick" ist ein spezieller Konstruktor von Bitmap, der Bereits einen Speicherbereich übergeben bekommt.
Und was bringt uns das?*Zugriff auf die Bitmap-Bits über byte-Array (kein unsafe) *Erzeugung eines "echten" Bitmaps, dass die selben Bits verwendet *schneller Zugriff über Array-Index, Klass-Indexer, Get/SetPixel *Natives Laden von bmp-Dateien

"schnell" bedeutet oben, dass (dank dem guten CLR-JITter) der Zugriff über das Array genauso schnell ist, wie ein unsafe-Code mit Zeigern.

Meine groben Messungen ergaben:
(GDI+)-GetPixel: 1x
MemBitmap.GetPixel: 30x
MemBitmap[x,y,c]-Indexer: 100x
MemBitmap.Data[index]: 1000x

(Also für jeden was dabei 😉)

Zukunft:
Die Unterstützung für die anderen PixelFormate (8BitIndexed, ...) sollen noch folgen.
(Momentan geht ja eigentlich nur 24Bit richtig)
sowie die native Unterstützung von GIF,PNG und JPEG-Dateien (dann auch JPEG2000). Und natürlich eine Speicherfunktion.

Insgesamt soll es also auch möglich sein, Bilder zu laden, zu manipulieren und wieder abzuspeichern ohne Überhaupt ein System.Drawing.Bitmap zu verwenden. (was Overhead bedeuten würde)

Code:


public class MemBitmap : IDisposable
{
    #region private Fields

        private int height;
        private int width;
        private int channels;

        private System.Drawing.Imaging.PixelFormat pixelformat;

        private byte[] data;
        private int stride;

        private System.Runtime.InteropServices.GCHandle dataHandle;
        private System.Drawing.Bitmap bitmap;

    #endregion

    #region public Properties

        public int Width { get { return width; } }
        public int Height { get { return height; } }
        public int Channels { get { return channels; } }

        public System.Drawing.Imaging.PixelFormat PixelFormat { get { return pixelformat; } }

        public byte[] Data { get { return data; } }
        public IntPtr DataPtr { get { return dataHandle.AddrOfPinnedObject(); } }

    #endregion

    #region Bitmap Compatibility

        public System.Drawing.Bitmap Bitmap
        {
            get
            {
                createBitmap();
                return bitmap;
            }
        }


        private void createBitmap()
        {
            if (bitmap == null)
            {
                //Bitmap erzeugen, die auf die selben Bit-Daten verweist
                bitmap = new System.Drawing.Bitmap(width, height, stride, pixelformat, dataHandle.AddrOfPinnedObject());
            }
        }

    #endregion

    #region Pixel Access

        public byte this[int x, int y, int channel]
        {
            get { return data[y * stride + x * channels + channel]; }
            set { data[y * stride + x * channels + channel] = value; }
        }

        public System.Drawing.Color GetPixel(int x, int y)
        {
            int index = y*stride + x*channels;

            switch(pixelformat)
            {
                case System.Drawing.Imaging.PixelFormat.Format24bppRgb:
                    return System.Drawing.Color.FromArgb(data[index + 2], data[index + 1], data[index + 0]);
                default:
                    throw new NotSupportedException();
            }
        }

        public void SetPixel(int x, int y, System.Drawing.Color value)
        {
            int index = y * stride + x * channels;

            switch (pixelformat)
            {
                case System.Drawing.Imaging.PixelFormat.Format24bppRgb:
                    data[index + 0] = value.B;
                    data[index + 1] = value.G;
                    data[index + 2] = value.R;
                    break;
                default:
                    throw new NotSupportedException();
            }
        }

    #endregion

    #region Initialization

        private void loadBitmap(string fileName)
    {
        System.IO.FileStream fs = null;
        try
        {
            //Dateistrom öffnen
            fs = new System.IO.FileStream(fileName, System.IO.FileMode.Open);

            //Zwischenspeicher anlegen
            byte[] tempMem = new byte[4];

            //Dateiheader einlesen
            uint    fileLength = 0;
            uint    imageDataOffset = 0;
            {

                //ersten beiden zeichen("BM")
                fs.Read(tempMem, 0, 2);
                if (tempMem[0] != 'B' || tempMem[1] != 'M')
                    throw new NotSupportedException("Unsupported Imagetype!");

                //Dateilänge
                fs.Read(tempMem, 0, 4);
                fileLength = BitConverter.ToUInt32(tempMem, 0);

                //reservierte Bits
                fs.Read(tempMem, 0, 4);

                //Offset der Bilddaten
                fs.Read(tempMem, 0, 4);
                imageDataOffset = BitConverter.ToUInt32(tempMem, 0);
            }

            //Bitmapheader einlesen
            uint    biHeaderSize = 0;
            int     biWidth = 0;
            int     biHeight = 0;
            ushort  biPlanes = 0;
            ushort  biBitCount = 0;
            uint    biCompression = 0;
            uint    biSizeImage = 0;
            int     biXPelsPerMeter = 0;
            int     biYPelsPerMeter = 0;
            uint    biClrUsed = 0;
            uint    biClrImportant = 0;
            {

                //Größe des BitmapHeaders
                fs.Read(tempMem, 0, 4);
                biHeaderSize = BitConverter.ToUInt32(tempMem, 0);

                //Breite
                fs.Read(tempMem, 0, 4);
                biWidth = BitConverter.ToInt32(tempMem, 0);

                //Höhe
                fs.Read(tempMem, 0, 4);
                biHeight = BitConverter.ToInt32(tempMem, 0);

                //Ebenen
                fs.Read(tempMem, 0, 2);
                biPlanes = BitConverter.ToUInt16(tempMem, 0);

                //BitsPerPixel
                fs.Read(tempMem, 0, 2);
                biBitCount = BitConverter.ToUInt16(tempMem, 0);

                //Kompression
                fs.Read(tempMem, 0, 4);
                biCompression = BitConverter.ToUInt32(tempMem, 0);

                //Byte-größe der Bilddaten
                fs.Read(tempMem, 0, 4);
                biSizeImage = BitConverter.ToUInt32(tempMem, 0);

                //Horizontale Auflösung
                fs.Read(tempMem, 0, 4);
                biXPelsPerMeter = BitConverter.ToInt32(tempMem, 0);

                //Vertikale Auflösung
                fs.Read(tempMem, 0, 4);
                biYPelsPerMeter = BitConverter.ToInt32(tempMem, 0);

                //Anzahl indizierter Farben
                fs.Read(tempMem, 0, 4);
                biClrUsed = BitConverter.ToUInt32(tempMem, 0);

                //Anzahl der zum darstellen benötigten indizierten Farben
                fs.Read(tempMem, 0, 4);
                biClrImportant = BitConverter.ToUInt32(tempMem, 0);
            }


            System.Drawing.Imaging.PixelFormat pf;
            switch (biBitCount)
            {
                case 24:
                    pf = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
                    break;
                default:
                    throw new NotSupportedException();
            }

            init(biWidth, biHeight, pf);

            if (biHeight < 0) //Bilddaten einlesen, wie sie kommen
            {
                fs.Read(this.data, 0, this.data.Length);
            }
            else //Bilddaten in umgekehrter Zeilenreihenfolge einlesen
            {
                for (int y = this.Height-1; y >= 0; y--)
                {
                    fs.Read(this.data, y * this.stride, this.stride);
                }
            }



        }
        finally
        {
            if (fs != null)
            {
                fs.Close();
            }
        }
    }

        private void init(int initWidth, int initHeight, System.Drawing.Imaging.PixelFormat initPixelFormat)
        {
            if (initWidth <= 0 || initHeight <= 0)
                throw new ArgumentOutOfRangeException("Negative Width or Height!");

            width = initWidth;
            height = initHeight;

            pixelformat = System.Drawing.Imaging.PixelFormat.Undefined;
            if (    initPixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb
                ||  initPixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb)
                pixelformat = initPixelFormat;
            else
                throw new ArgumentOutOfRangeException("PixelFormat not supported!");


            switch (pixelformat)
            {
                case System.Drawing.Imaging.PixelFormat.Format24bppRgb:
                    channels = 3;
                    break;
                case System.Drawing.Imaging.PixelFormat.Format32bppArgb:
                    channels = 4;
                    break;
                default:
                    throw new InvalidOperationException();
            }

            stride = (channels * width);
            int rest = stride % 4;
            if (rest != 0)
                stride += (4 - rest);

            data = new byte[stride * height];
            dataHandle = System.Runtime.InteropServices.GCHandle.Alloc(data, System.Runtime.InteropServices.GCHandleType.Pinned);
        }

        private void init(System.Drawing.Bitmap b)
        {
            init(b.Width, b.Height, b.PixelFormat);
            System.Drawing.Imaging.BitmapData bd = b.LockBits(new System.Drawing.Rectangle(0, 0, b.Width, b.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, b.PixelFormat);
            System.Runtime.InteropServices.Marshal.Copy(bd.Scan0, data, 0, data.Length);
            b.UnlockBits(bd);
        }

    #endregion

    #region Constructors


        public MemBitmap(int initWidth, int initHeight, System.Drawing.Imaging.PixelFormat initPixelFormat)
        {
            init(initWidth, initHeight, initPixelFormat);
        }

        public MemBitmap(System.Drawing.Bitmap b)
        {
            init(b);           
        }


        public MemBitmap(string fileName)
        {
            if (fileName.EndsWith(".bmp"))
            {
                try
                {
                    loadBitmap(fileName);
                    return;
                }
                catch { }
            }

            System.Drawing.Bitmap b = new System.Drawing.Bitmap(fileName);
            init(b);
            b.Dispose();
        }

    #endregion

    #region Destruction

        public void Dispose()
        {
            if (bitmap != null)
            {
                bitmap.Dispose();
            }
            if (dataHandle != null)
            {
                dataHandle.Free();
            }
            data = null;
        }

    #endregion

}

beste Grüße
zommi

Schlagwörter: <Bitmap, GetPixel, SetPixel, LockBits, Bildverarbeitung>

Gelöschter Account
vor 15 Jahren

vielen dank für die klasse. sowas habe ich echt vermisst.

wäre es eigendlich sinnvoll die klasse als nicht serialisierbar zu kennzeichnen? immerhin ist das bitmap im heap gepinnt und der addressbasierte zugriff wäre eigendlich bei einem deserialisiertem objekt nicht mehr gültig, außer man implementiert ISeralisable und IDeserializationCallback.

weiters möchte ich hier erwähnen, das eben weil es gepinnt ist, es vorkommen kann (z.b. bei vielen kleinen bildern, die man mit der klasse lädt), das der heap fragmentiert. man erkauft sich eben diesen performancegewinn....

ist in zukunft ein .Save(...) geplant, ohne das man auf die framework-bitmap-klasse ausweichen muss, da das nicht nur ressourcen frisst, sondern auch performance und code-aufwand bedeutet?

und noch etwas kleineres. MarshalByRefObject erben?^^ das wär noch cool. so wäre der weg frei für remoting.

macht es sinn das ImmutableObject(true) attribut zu setzten? (hier bin ich mir nicht ganz sicher ... ist das jetzt ein rein designerrelevantes attribut oder hat es noch mehr zu bedeuten?)

nochmals vielen vielen lieben dank für diese klasse. ein addressbasiertes und vor allem schnelles editieren einer bitmap ist etwas, was das framework echt vermisst.

ach ja:

default:
                    throw new NotSupportedException();

da sagt microsoft: exception by default is not a good style.

aber das nur am rande.

zommi Themenstarter:in
1.361 Beiträge seit 2007
vor 15 Jahren

Hi,

vielen dank für die klasse.

Bitte bitte 🙂

nicht serialisierbar zu kennzeichnen?

Mhh. klingt sinnvoll. Oder aber man verwendet wirklich ISerialisable und updatet es dann entsprechend.
Man müsste dann nur das zugehörige Bitmap disposen und verwerfen/neu erstellen. Ich glaube nicht, dass man dort (safe) den internen Zeiger so einfach umbiegen kann.
(Also per Reflection wärs möglich, ist ja "nur" ein privates Feld. Aber sollte man Bitmap wirklich die Privatsphäre nehmen? 😉)

heap fragmentiert

Man könnte das Pinnen ja auch lazy machen.
Da man die feste Position ja erst brauche, wenn jemand das Bitmap Objekt haben will.
(Später solls ja so sein, dass man dies nur will, wenn man das MemBitmap anzeigen will. Und dann könnte man ja ein allgemeines Anzeige-MemBitmap verwenden und dann das auszugeben Bild da kurz reinkopieren. Dann hat man viele unpinned MemBitmaps und ein MemBitmap (gepinned) mit Bitmap)
Aber: Da i.A. eh alle Objekte über 85000 bytes auf dem LOH (large object heap) landen und bereits ein 240*120 großes 24Bit bild darüber liegt, landen die eh alle dort und werden nich verschoben, ob nun gebpinned oder nicht.
Aber klar, bei kleineren Bildern machts was aus.
Dann scheint das lazy pinnen doch besser zu sein oda ?

Ein Save ist natürlich geplant, MarshalByRefObject... hab mich mit Remoting noch nicht beschäftigt. kA ob das geht 😉
ImmutableObject... genauso. kA 🙂
(werd ich mir mal anschaun)

beste Grüße
zommi

Gelöschter Account
vor 15 Jahren

Ein Save ist natürlich geplant, MarshalByRefObject... hab mich mit Remoting noch nicht beschäftigt. kA ob das geht

simpel... einfach erben und das wars.

Dann scheint das lazy pinnen doch besser zu sein oda ?

lazy und die möglichkeit es explizit zu machen ist denke ich am besten.

Man müsste dann nur das zugehörige Bitmap disposen und verwerfen/neu erstellen oder mit serialisieren.

zommi Themenstarter:in
1.361 Beiträge seit 2007
vor 15 Jahren

Man müsste dann nur das zugehörige Bitmap disposen und verwerfen/neu erstellen
oder mit serialisieren.

mhh.. schwierig.
Referenzen werden ja beim binary-serialisieren wieder neu hergestellt. Aber Bitmap hat ja intern bloß nen IntPtr auf das Object. Das wird doch wohl kaum klappen, dass der automatisch neu zugewiesen wirde?!?!

beste Grüße
zommi

Gelöschter Account
vor 15 Jahren

bitmap ist aber bereits serialaisierbar.

0
767 Beiträge seit 2005
vor 15 Jahren

hab den code nur überflogen aber aufgefallen ist mir dabei:

wärs nicht möglich (zwecks Polymorphie) direkt von Image zu erben?

loop:
btst #6,$bfe001
bne.s loop
rts

Gelöschter Account
vor 15 Jahren

wärs nicht möglich (zwecks Polymorphie) direkt von Image zu erben?

in diesem fall nein bzw überhaupt nicht sinnvoll. dafür gibt es aber auch das propertie

public System.Drawing.Bitmap Bitmap
        {
            get
            {
                createBitmap();
                return bitmap;
            }
        }

zommi Themenstarter:in
1.361 Beiträge seit 2007
vor 15 Jahren

Hi,

wärs nicht möglich (zwecks Polymorphie) direkt von Image zu erben?

Eigentlich war ja die Idee komplett ohne GDI+ auszukommen, ohne den Overhead und teilweise das Problem, dass da dann evtl mal die Handles aufgebraucht sind.
Schließlich heißt es MemBitmap, was auf Memory hindeuten soll 😉
Also Bilder die erstmal nur was für den Speicher zum rumwerkeln sind, und evtl. noch aus Kompatibilitätsgründen die Möglichkeit bieten unter Umständen ein GDI+-Bitmap draus zu machen, um es direkt anzeigen zu können. 😉

Aber so schlecht find ich die Idee prinzipiell nicht.
Vielleicht sollte man ein Interface nehmen und dann einzelne Bitmap# klassen anlegen, eine von Image abgeleitet, eine komplett unabhängig... mhhh 😉

Und zu:

bitmap ist aber bereits serialaisierbar.

Jup stimmt. ich habs grad probiert.
Wenn ich das Bitmap darin serialisier/deserialisier dann kopiert der sich den kompletten Byte-Array-Block und dann gibs den zweimal im Speicher... Ratte 😉
Ich kann also nicht die normale Bitmap serialisierung intern nehmen. Entweder mit Surrogaten irgendwie anders oder einfach neu erstellen.

beste Grüße
zommi

5.657 Beiträge seit 2006
vor 15 Jahren

Im Framework 3.5 gibt es für diese Zwecke auch eigene Bitmap-Objekte, z.B. die WriteableBitmap-Klasse oder (noch schneller) die InteropBitmap-Klasse.

Hier gibt es einen schönen Artikel dazu, in dem der Algorithmus für das Dragon-Line Fraktal umgesetzt wird: How to: High performance graphics in WPF

Schöne Grüße,
Christian

Weeks of programming can save you hours of planning

U
208 Beiträge seit 2008
vor 15 Jahren

Hi zommi,

und vielen Dank für die Klasse. Hab sie schon in Verwendung, wie du bereits weißt. 🙂

Planst du vielleicht in nächster Zeit eine Unterstützung von weiteren PixelFormats, außer Format24bppRgb? Momentan gehe ich so vor, dass ich aus einem in die Form geladenem Image-Objekt zuerst ein Bitmap-Objekt erzeuge, bei diesem dann das PixelFormat in Format24bppRgb ändere (in dem ich per Clone ein neues Bitmap-Objekt erzeuge) und aus diesem Bitmap-Objekt dann letzlich ein MemBitmap-Objekt erzeuge. Klappt alles, ich habe allerdings den Nachteil, dass ich keine transparenten Bilder verarbeiten kann, bzw. verarbeiten schon, aber die Transparenz-Informationen gehen leider verloren.

Grüße,

Isaac

4.931 Beiträge seit 2008
vor 12 Jahren

Hallo zommi,

mir ist gerade ein kleiner Fehler bei dem MemBitmap-Konstruktor aufgefallen:


public MemBitmap(int initWidth, int initHeight, System.Drawing.Imaging.PixelFormat initPixelFormat)
{
    init(initWidth, initHeight, initPixelFormat); // nicht 'pixelformat'
}

zommi Themenstarter:in
1.361 Beiträge seit 2007
vor 12 Jahren

Hi Th69,

danke! soeben ausgebessert.

beste Grüße
zommi