Hallo zusammen,
ich bin gerade dran, ein Programm für eine Bild-Stapelverarbeitung zu entwickeln. Soll heißen: Der Benutzer kann eine beliebige Anzahl von Bildern auswählen und diese mit beliebig vielen Bearbeitungsschritten bearbeiten (bsp. Rahmen setzen, Schrift einfügen, Größe ändern, Drehen, usw.)
Nun möchte ich Filter einbauen. Das heißt zum Beispiel eine Schwarz/Weiss-Konvertierung, Sepia oder die Veränderung des Kontrastes. Dazu gibt es endlos viele Möglichkeiten.
Mit dem .NET Framework 3.5 kamen die PixelShader. Leider können diese wohl nicht auf ein BitmapSource angewendet werden. Ich habe im Internet nirgends eine Lösung gefunden.
Ein weitere Möglichkeit wäre die Verarbeitung auf Pixelebene. Allerdings ist diese erstens langsam und zweitens kompliziert - ich wüsste beispielsweise nicht wie ich dann den Kontrast erhöhen könnte, ganz geschweige von vielen anderen Szenarien.
Bei System.Drawing gibt es die ColorMatrix, mit der man viel machen konnte. Leider scheint diese in WPF zu fehlen. Und jedes mal ein BitmapSource in ein Bitmap umwandeln und wieder zurück wird sehr viel Zeit beanspruchen.
Nun stehe ich da und finde keine richtige Lösung. PixelShader fände ich sehr interessant. Erstens sind diese sehr schnell und zweitens gibt es einige Beispiele. Nur doof, dass ich sie anscheinend technisch nicht verwenden kann.
Habt ihr eventuell eine Lösung für mich - wie ich die Bilder möglichst schnell verarbeiten kann und wozu es auch einige Beispiele gibt? Vielen Dank für eure Hilfe.
Ich arbeite mit dem .NET Framework 4.0
Grüße
Dennis
Eigendlich gibt es dafür nur 2 Lösungen.
Die Pixelshader, welche sich allerdings nur umständlich auf eine BitmapSource anwenden lassen:
-BitmapSource in einem ImageControl anzeigen
-Shader aufs ImageControl anwenden
-Dafür sorgen dass das ImageControl platziert wird (Arrange())
-Per RenderTargetBitmap einen Screenshot vom ImageControl machen
Die RenderTargetBitmap die am Schluss rauskommt ist dann wieder eine BitmapSource
Manuell alle Pixel durchlaufen. Das ist wie bereits gesagt etwas langsamer als die Shader. Wenn du mit dieser Methode Performance rausschlagen willst wird dir wohl nichts anderes übrig bleiben als ihn auf C-Ebene auszulagen. Nativer Code ist ja bekanntlich schneller. Zuerst würde ich das allerdings erst mal ausprobieren. Evtl. ist es ja gar nicht soooo langsam.
Ein Kontrastfilter wird allerdings bei beiden Varianten gleich komplex sein da du ihn selbst programmieren musst. Wenn du etwas darüber nachdenkst gestaltet sich das allerdings meißt als weniger kompliziert wie man zunächst annimmt.
Hier z.B. der HLSL-Code (Shader) für einen Sättigugnsfilter aus meinem Code:
float4 main(float2 uv : TEXCOORD) : COLOR
{
//Quellpixel holen
float4 srcPixel = tex2D(input, uv);
//Helligkeits-Wert anhand des Vektorprodukts vom Quellpixel und einem Standardwert berechnen
//Der Standardwert ist weit verbreitet und wird auch von NTSC und JPEG verwendet
float luminance = dot(srcPixel,float3(0.299,0.587,0.114));
//Mit einer Linearen interpolation (lerp) einen Wert zwischen den ausgegrauten wert und dem OriginalPixel ermitteln
float4 dstPixel = lerp(luminance,srcPixel,saturationValue);
//Alpha-Kanal bleibt unberührt
dstPixel.a = srcPixel.a;
return dstPixel;
}
Solche Code-Schnipsel solltes du auch massig im Internet finden. Die werden in die Spieleprogrammierung schon seit zig Jahren eingesetzt (wenn auch weniger in der C#-Welt (ausser XNA)). Siehe hier: http://www.gamedev.net/
Wenn du die Berechnung per Software machst wirst du warscheinlich den gleichen Code einfach auf C# (oder C) übersetzen müssen. Das sollte nicht allzu kompliziert sein.
Ganz genau das ist das schöne an den PixelShadern: Es gibt massenhaft Beispiele, also könnte ich sehr viele Filter anbieten. Ganz abgesehen von den anderen Problemen, ist es überhaupt möglich auf ein ImageControl mehrere PixelShader anzuwenden? Dazu noch eine Frage: Wenn die das Bild auf der Oberfläche anzeige um ein Screenshot zu machen - dann habe ich doch (alleine von der Größe her) einen immensen Qualitätsverlust gegenüber dem originalen, oder? In den meisten Fällen werde ich das ImageControl nach nicht so groß wie das originale Bild machen können, ganz geschweige davon, dass das der Benutzer ja nicht sehen soll.
Mit C habe ich leider noch nie gearbeitet. Wenn das mit den PixelShadern nicht funktioniert werde ich wohl wirklich versuchen, den HLSL-Code in c#-Code umzuwandeln.
Du kannst das ImageControl genauso groß wie das Original machen.
Irgendwo gibt es dann allerdings schon eine Grenze. Ich glaube wenn du es 100.000 Pixel groß machst gibts tatsächlich ne Exception.
Du kannst deinen Filter allerdings auf die besagte weise immer wiederholen. Wenn du den Filter einmal angewendet hast nimmst du einfach wieder ein ImageControl, steckst deine RenderTargetBitmap rein (auf die der 1. Filter bereits angewendet wurde) und wendest den nächsten Filter an.
Alternativ schachtelst du einfach dein ImageControl in mehreren Content-Controls auf die du auch Filter anwenden kannst. Was performanter ist kann ich dir jetzt leider nicht sagen.
Den HLSL-Code darfst du NICHT in C# umwandeln wenn du Shader baust. Die werden dann schon in HLSL bleiben müssen. Du benötigst lediglich einen C#-Wrapper ums HLSL.
Ausserdem ist es auch möglich ein ImageControl so zu platzieren dass es NICHT am Bildschirm sichtbar ist. Rufe dazu einfach die Arrange()-Methode auf. Danach kannst du RenderTargetBitmap verwenden. Das Control wird nicht wirklich angezeigt.
Achso - ich dachte, dass würde nur funktionieren, wenn das ImageControl auf der Oberfläche sichtbar ist. Also wird ja nicht direkt Screenshot gemacht. Hast du vielleicht schon ein funktionierendes Beispiel dazu?
Ich hätte den HLSL-Code nur in c# umgewandelt, wenn ich die PixelShader nicht benutzen würde. 😉
Hier ein Code-Schnipsel aus einem alten Testprojekt von mir. Dabei wir ein XAML-Ausschnitt gerendert und als Bitmap abgespeichert:
int x = Int32.Parse(txbX.Text.Trim()), y = Int32.Parse(txbY.Text.Trim());
ContentPresenter icon = new ContentPresenter() { VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Left };
icon.Content = deserialize();
icon.Measure(new Size(x, y));
icon.Arrange(new Rect(0, 0, x, y));
Console.WriteLine(icon.DesiredSize.ToString());
RenderTargetBitmap rtb = new RenderTargetBitmap((int)Math.Ceiling(icon.DesiredSize.Width),
(int)Math.Ceiling(icon.DesiredSize.Height), 96, 96, PixelFormats.Pbgra32);
rtb.Render(icon);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
FileStream stream = new FileStream("Icon.png", FileMode.Create, FileAccess.ReadWrite);
encoder.Save(stream);
stream.Close();
Den Code kannst du ja beliebig anpassen und auf ImageControls umstellen.
Wichtig ist dass du bei den 96 die richtigen DPI-Werte des PCs einträgst.
Vielen Dank für das Beispiel. Ich habe es mal versucht auf mein Szenario umzuändern:
// Bild und Effekt laden
this.imageControl.Source = new BitmapImage(new Uri(@"C:\test\go.jpg"));
this.imageControl.Effect = new MyEffect(@"C:\test\ps\ZoomBlur.ps");
this.imageControl.Measure(new Size(this.imageControl.Source.Width, this.imageControl.Source.Height));
this.imageControl.Arrange(new Rect(0, 0, this.imageControl.Source.Width, this.imageControl.Source.Height));
// DPI herausfinden
float dpi = System.Drawing.Graphics.FromImage(System.Drawing.Image.FromFile((@"C:\test\go.jpg"))).DpiX;
// RenderTargetBitmap erstellen
RenderTargetBitmap rtb = new RenderTargetBitmap(
Convert.ToInt32(this.imageControl.Source.Width),
Convert.ToInt32(this.imageControl.Source.Height),
dpi, dpi,
PixelFormats.Pbgra32);
// RenderTargetBitmap rendern
rtb.Render(this.imageControl);
// Neues Bild speichern
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
FileStream stream = new FileStream(@"C:\test\go2.jpg", FileMode.Create, FileAccess.ReadWrite);
encoder.Save(stream);
stream.Close();
Effekt-Klasse:
public class MyEffect : ShaderEffect
{
public MyEffect(string file)
{
this.PixelShader = new PixelShader()
{
UriSource = new Uri(file, UriKind.Absolute)
};
}
}
Irgendetwas mache ich falsch, denn leider komme ich zu dem Ergebnis wie im Anhang.
Versuch zuerst mal in XAML den Shader anzuwenden. Dadurch kannst du testen ob der funktioniert. Wäre möglich dass der für den Fehler verantwortlich ist.
Ich habe es mal folgendermaßen versucht:
this.imageControl.Source = new BitmapImage(new Uri(@"C:\test\go.jpg"));
this.imageControl.Effect = new MyEffect(@"C:\test\ps\ZoomBlur.ps");
Das Bild blieb auf der Oberfläche unverändert. Komisch - ich hatte im Kopf, dass dieser Filter funktioniert. Nun habe ich einen anderen genommen, welcher mit den obigen beiden Zeilen funktioniert (InvertColor). Wenn ich das Bild allerdings speichere, ist dies komplett schwarz.
Irgendwie funktioniert das mit dem Rendern wohl nicht.
Das ist interessant.. ich hatte vorher die Postion des ImageControls auf der Oberfläche vorher mit Margins festgelegt und _Stretch _auf "Uniform" gestellt.
Nun habe ich die Margins entfernt und _Stretch _auf "None" gestellt.
<Image Name="imageControl" Stretch="None" />
Ergebnis: Das gespeicherte Bild ist nun nicht schwarz - jetzt ist es hellblau. Hä? 😃
Mach doch mal ein Testprojekt welches ein Fester anzeigt.
Darin ist das Bild mit Shader angewendet. Ein Button daneben macht einen Screenshot vom ImageControl und speichert es als PNG/JPG ab.
Dadurch kannst du eingrenzen wo das Problem eintritt.
Auf der Oberfläche wird der Effect problemlos angewendet. Ich habe das Testprojekt mal in den Anhang getan.
@Dennisspohr du kannst auch hier nachsehen:
falls Du vielleicht einige Filter verwenden möchtest, die so nicht so einfach zu realisieren sind. Allerdings verwendet diese Library an vielen Stellen eine Per-Pixel-Methode im unsafecontext.
Also als wirklich langsam würde ich das nicht gerade bezeichnen bei konkret diesen Filtern.
EDIT: Link korrigiert.
Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.
Hallo dr4g0n76,
wenn ich auf deinen Link klicke bekomme ich folgende Fehlermeldung:
Die Seite wurde gelöscht ODER der Zugriff auf diese Seite wurde verweigert.
Hatte den falschen Link. Jetzt müsstest Du drauf klicken können. Versuchs nochmal.
Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.
Hallo,
die Idee mit den Shadern mag aufm ersten Blick sinnvoll erscheinen, ist sie aber absolut nicht.
Shader in WPF verändern nachträglich die Darstellung der GUI. Diese GUI Darstellung dann wieder als Bild speichern zu wollen ist nicht zielführend.
Wenn du deine Bilder (sprich Daten) verarbeiten und irgendwelche Filter drauf anwenden möchtest, dann tu das doch auch (und nicht versuchen durch Effekte in der GUI die die Daten anzeigt irgendwas erreichen zu wollen). Das das einen gewissen Aufwand bedeutet und Zeit dauert ist doch klar, von nichts kommt nichts. Mit der WriteableBitmap Klasse besitzt WPF eine Klasse mit der du recht performant arbeiten kannst. Dazu wie einzelne Filter funktionieren hat ja schon dr4g0n76 den passenden Link gepostet (auf den ich auch verweisen wollte g)
Baka wa shinanakya naoranai.
Mein XING Profil.
dr4g0n76, vielen Dank für deinen Link. Das sieht sehr gut und vielseitig aus! Ich muss nur gucken, wie ich das ganze in WPF abbilden kann..
Ja talla, du hast Recht.
Ich werde mich mal damit beschäftigen und dann versuchen mit der WriteableBitmap zu arbeiten. Mal sehen ob mir das gelingt 😃
Vielen Dank & Grüße
Dennis
Ich habe hier gerade noch etwas entdeckt:
http://aviary.com/online/filter-editor#
Das macht etwas ähnliches wie du implementieren willst. Vielleicht interressiert dich das.
Danke, ich werds mir gleich mal anschauen.