Performante Bildverarbeitung in WPF mit InteropBitmap

So schön es auch ist in WPF zu programmieren, so schwer ist es auch eine performante Anwendung zu schreiben. WPF wurde hauptsächlich für Vektorgrafiken entworfen. Diese sind sehr rechenintensiv und gerade bei vielen Objekten geht die Leistung damit deutlich in den Keller. Die Performance einer Oberfläche ist jedoch sehr wichtig. Der Benutzer möchte schnell arbeiten und erwartet dabei eine geringe Prozessorauslastung Rastergrafiken bieten im Gegensatz zu Vektorgrafiken eine höhere Performance. Wirklich effizient kann man mit ihnen allerdings nur arbeiten, wenn man die einzelnen Pixel direkt ansprechen kann. WPF selbst liefert dafür leider keine direkte Möglichkeit.
Es ist zwar möglich Bilder aus einem Bytearray zu gewinnen, dies muss allerdings für jedes Bild einzeln geschehen. Bei Anwendungen die eine schnelle Bildfolge erfordern, wie etwa ein Videoplayer oder Framegrabber, ist das unnötiger Mehraufwand.
Um das Bild nur einmal erstellen zu müssen und direkt in den Bildspeicher schreiben zu können bietet WPF das InteropBitmap an. Allerdings ist dessen Benutzung nicht so einfach, da hier unsicherer Code verwendet werden muss, aber fangen wir am besten einfach an.



Wir erstellen eine kleine Anwendung in WPF in der über drei Buttons die Farbe des Bildes gewechselt werden kann. Als erstes muss der XAML-Code für die Anwendung geschrieben werden. Da es sich hier nur um ein Bild mit drei Buttons handelt dürfte der Code selbsterklärend sein.
<Window x:Class="IOBitmap.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="324" Width="432">
<Grid>
<Image Margin="0,0,0,30" Name="myImage" Stretch="Fill" />
<Button Height="23" Margin="0,0,0,1" Name="btnBlue" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="74" Click="btnBlue_Click">Blau</Button>
<Button Height="23" HorizontalAlignment="Left" Margin="80,0,0,1" Name="btnRed" VerticalAlignment="Bottom" Width="74" Click="btnRed_Click">Rot</Button>
<Button Height="23" Margin="160,0,176,1" Name="btnGreen" VerticalAlignment="Bottom" Click="btnGreen_Click">Grün</Button>
</Grid>
</Window>

Bevor wir mit dem Code anfangen können müssen wir in unserem Projekt erst einmal unsicheren Code zulassen. Das bedeutet nichts weiter, als das wir das verwenden von Zeigern, wie man sie aus C++ kennt zulassen. Da das .Net-Framework einen GC besitzt ist es jedoch nicht so einfach damit zu arbeiten. Im Normalfall sollte man es deshalb lieber lassen und die gesamte Speicherverwaltung dem GC überlassen. Um unsicheren Code zuzulassen muss man auf die Eigenschaften des Projektes gehen und den entsprechenden Hacken setzen.

Nun können wir damit beginnen uns an den Code zu wagen. Das hier ist der gesamte Code für unser Beispiel. Dieser Code steht in der cs-Datei von Window1.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Interop;
using System.Runtime.InteropServices;

namespace IOBitmap
{
/// 
/// Interaktionslogik für Window1.xaml
/// 
public partial class Window1 : Window
{
const uint FILE_MAP_ALL_ACCESS = 0xF001F;
const uint PAGE_READWRITE = 0x04;

private uint _width = 300;
private uint _height = 400;
private int _bpp = PixelFormats.Bgr32.BitsPerPixel / 8;
private int _stride;
private InteropBitmap _InteropBitmap;
private IntPtr _ptr;
unsafe private int* _vptr;

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes,
uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow,
string lpName);

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,
uint dwDesiredAccess,
uint dwFileOffsetHigh,
uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);

public Window1()
{
InitializeComponent();

_ptr = CreateFileMapping(new IntPtr(-1),
IntPtr.Zero,
PAGE_READWRITE,
0,
_width * _height * (uint)_bpp,
null);

_stride = (int)_width * _bpp;
_InteropBitmap = System.Windows.Interop.Imaging.CreateBitmapSourceFromMemorySection(_ptr,
(int)_width,
(int)_height,
PixelFormats.Bgr32,
_stride,
0) as InteropBitmap;
this.myImage.Source = _InteropBitmap;

unsafe
{
_vptr = (int*)MapViewOfFile(_ptr,
FILE_MAP_ALL_ACCESS,
0,
0,
_width * _height * 4).ToPointer();
}
}

private void DrawColor(int color)
{
unsafe
{
for (int i = 0; i < _width * _height; i++)
{
_vptr[i] = color;
}
}
_InteropBitmap.Invalidate();
}

private void btnBlue_Click(object sender, RoutedEventArgs e)
{
DrawColor((int)0x0000FF);
}

private void btnRed_Click(object sender, RoutedEventArgs e)
{
DrawColor((int)0xFF0000);
}

private void btnGreen_Click(object sender, RoutedEventArgs e)
{
DrawColor((int)0x00FF00);
}
}
}
Am Anfang muss der benötigte Speicherplatz für das Bild im Arbeitsspeicher angelegt werden. Dafür benötigt man die Farbtiefe des Bildes sowie dessen Breite und Höhe. Bei uns stehen diese Werte in _bpp, _width und _height.
_ptr = CreateFileMapping(new IntPtr(-1),
IntPtr.Zero,
PAGE_READWRITE,
0,
_width * _height * (uint)_bpp,
null);
Der IntPtr _ptr enthält nun ein Handle auf den angelegten Speicherbereich. Da wir den Speicherbereich nun für uns reserviert haben können wir aus diesem unser Bild generieren.
_stride = (int)_width * _bpp;
_InteropBitmap = System.Windows.Interop.Imaging.CreateBitmapSourceFromMemorySection(_ptr, (int)_width, (int)_height, PixelFormats.Bgr32, _stride, 0) as InteropBitmap;
this.myImage.Source = _InteropBitmap;
Zuerst wird die Variable _stride zugewiesen. In ihr steht die Anzahl der Bytes für eine Linie unseres Bildes. Danach erstellen wir unser InteropBitmap. Das generierte InteropBitmap fügen wir anschließend mit myImage.Source als Bildquelle zu unserem Bild. Das Bild ist dadurch nun fest mit den Informationen, die im Arbeitsspeicher stehen, verknüpft. Mit unserem IntPtr können wir jedoch nicht auf den Speicherbereich zugreifen. Um das jedoch zu können kommt nun unsicherer Code ins Spiel. Mittels mapping erstellen wir einen Integer-Zeiger auf den Speicherbereich, mit der genauen Adresse im Arbeitsspeicher.
unsafe
{
_vptr = (int*)MapViewOfFile(_ptr,
FILE_MAP_ALL_ACCESS,
0,
0,
_width * _height * 4).ToPointer();
}
Über den Zeiger _vptr können wir nun auf den gesamten Bereich wahlfrei zugreifen. Um das ganze zu demonstrieren iteriere ich in DrawColor einmal über den ganzen Bereich und ändere die Werte.
private void DrawColor(int color)
{
unsafe
{
for (int i = 0; i < _width * _height; i++)
{
_vptr[i] = color;
}
}
_InteropBitmap.Invalidate();
}
Hierbei muss auch wieder darauf geachtet werden unsicheren Code zu benutzen, da wir hier mit dem Integer-Zeiger arbeiten. Das gute an einer Farbtiefe von 32bit ist, dass man hier direkt mit dem Zeiger Arbeiten kann, da ein Integer auch 32bit besitzt. Nachdem alle Arbeiten getan sind und das Bild verändert wurde muss jedes mal Invalidate aufgerufen werden. Damit wird WPF gezwungen das Bild neu zu rendern. Ein kleiner Tipp noch zum Schluss: Wer mit Bgra32 arbeiten möchte, da dieses Format einen Alphawert besitzt kann auf Probleme stoßen. Visual Studio meckert hier, da eine große Zahl hierbei das Bit für das Vorzeichen überschreibt. Mit unchecked sollte es jedoch kein Problem sein mit hexadezimalen Zahlen zu arbeiten.
unchecked _vptr[i] = (int)0xFFFFFFFF;
Bookmark and Share

3 Kommentare:

Anonym hat gesagt…

Wenn man _InteropBitmap.Invalidate(); häufig aufruft, dann steigt der RAM Verbrauch stetig an um über 1,5 GB und CPU ist dementsprechend bei 90 % :

Z.B. via timer event alle 30 msec _InteropBitmap.Invalidate(); aufrufen bei einer width = 1000 und height = 800.
Da ganze ohne/unabhängig von den Pixel-manipulationen (die habe ich auskommentiert).

Es scheint ein memory leak Problem zu geben mit InteropBitmap?

Anonym hat gesagt…

Ab .NET 4.0 gibt es ein memory leak mit InteropBitmap:

http://connect.microsoft.com/VisualStudio/feedback/details/603004/massive-gpu-memory-leak-with-interopbitmap

Groupdmt hat gesagt…

Really i appreciate the effort you made to share the knowledge.The topic here i found was really effective to the topic which i was researching for a long time
Retusche Dienstleistung

Kommentar veröffentlichen