Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 | Suche | FAQ

Hauptmenü
myCSharp.de
» Startseite
» Forum
» Suche
» Regeln
» Wie poste ich richtig?

Mitglieder
» Liste / Suche
» Wer ist online?

Ressourcen
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Microsoft Docs

Team
» Kontakt
» Cookies
» Spenden
» Datenschutz
» Impressum

  • »
  • Community
  • |
  • Diskussionsforum
Bitmap-Manipulation (MemBitmap)
zommi
myCSharp.de - Member

Avatar #avatar-2617.png


Dabei seit:
Beiträge: 1.361
Herkunft: Berlin

Themenstarter:

Bitmap-Manipulation (MemBitmap)

beantworten | zitieren | melden

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>
Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von zommi am .
private Nachricht | Beiträge des Benutzers
Gelöschter Benutzer

beantworten | zitieren | melden

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.


[offtopic]
ach ja:

default:
                    throw new NotSupportedException();
da sagt microsoft: exception by default is not a good style.

aber das nur am rande.
[/offtopic]
zommi
myCSharp.de - Member

Avatar #avatar-2617.png


Dabei seit:
Beiträge: 1.361
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hi,
Zitat von JAck30lena
vielen dank für die klasse.
Bitte bitte
Zitat von JAck30lena
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? )
Zitat von JAck30lena
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
private Nachricht | Beiträge des Benutzers
Gelöschter Benutzer

beantworten | zitieren | melden

Zitat
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.
Zitat
Dann scheint das lazy pinnen doch besser zu sein oda ?
lazy und die möglichkeit es explizit zu machen ist denke ich am besten.
Zitat
Man müsste dann nur das zugehörige Bitmap disposen und verwerfen/neu erstellen
oder mit serialisieren.
zommi
myCSharp.de - Member

Avatar #avatar-2617.png


Dabei seit:
Beiträge: 1.361
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Zitat
Zitat
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
private Nachricht | Beiträge des Benutzers
Gelöschter Benutzer

beantworten | zitieren | melden

bitmap ist aber bereits serialaisierbar.
0815Coder
myCSharp.de - Member



Dabei seit:
Beiträge: 767

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Gelöschter Benutzer

beantworten | zitieren | melden

Zitat
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
myCSharp.de - Member

Avatar #avatar-2617.png


Dabei seit:
Beiträge: 1.361
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hi,
Zitat
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:
Zitat
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
private Nachricht | Beiträge des Benutzers
MrSparkle
myCSharp.de - Team

Avatar #avatar-2159.gif


Dabei seit:
Beiträge: 5.655
Herkunft: Leipzig

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
userid12529
myCSharp.de - Member



Dabei seit:
Beiträge: 208

beantworten | zitieren | melden

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
private Nachricht | Beiträge des Benutzers
Th69
myCSharp.de - Experte

Avatar #avatar-2578.jpg


Dabei seit:
Beiträge: 4.389

beantworten | zitieren | melden

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'
}
private Nachricht | Beiträge des Benutzers
zommi
myCSharp.de - Member

Avatar #avatar-2617.png


Dabei seit:
Beiträge: 1.361
Herkunft: Berlin

Themenstarter:

beantworten | zitieren | melden

Hi Th69,

danke! soeben ausgebessert.

beste Grüße
zommi
private Nachricht | Beiträge des Benutzers