我在 C# WPF 中制作了一個簡單的應用程式,它從影像中顯示 ascii 藝術,代碼似乎可以作業,但問題是每當我將文本塊的文本設定為 ascii 藝術時,它似乎都有奇怪的換行符,無關用我的代碼
文本塊中的 ASCII

輸出日志中的 ASCII

在這里您可以看到輸出日志正確顯示了藝術作品,而文本塊有換行符,我也嘗試了 TextBox 和 RichTextBox,它們給了我相同的結果
影像到 Ascii 藝術代碼
void TurnImageToAscii(Bitmap image)
{
StringBuilder sb = new StringBuilder();
for (int j = 0; j < image.Height; j )
{
for (int i = 0; i < image.Width; i )
{
Color pixelColor = image.GetPixel(i,j);
int brightness = (pixelColor.R pixelColor.G pixelColor.B) / 3;
int charIndex = brightness.Remap(0, 255, 0, chars.Length - 1);
string c = chars[charIndex].ToString();
sb.Append(c);
}
sb.Append("\n");
}
asciiTextBlock.Text = sb.ToString();
}
我該如何解決這個問題?
編輯: 重映射功能
public static class ExtensionMethods
{
public static int Remap(this int value, int from1, int to1, int from2, int to2)
{
return (value - from1) / (to1 - from1) * (to2 - from2) from2;
}
}
字符字串:
string chars = " .:-^$@";
這是宣告 chars 陣列的地方,并且它只在TurnImageToAscii我在上面的問題中發布的函式中被參考過一次
XAML
<Window x:Class="AsciiImageWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AsciiImageWPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid Loaded="Grid_Loaded">
<ComboBox x:Name="videoDevicesList" HorizontalAlignment="Left" Margin="24,22,0,0" VerticalAlignment="Top" Width="419"/>
<Button x:Name="start_btn" Content="Start" HorizontalAlignment="Left" Margin="448,22,0,0" VerticalAlignment="Top" Height="22" Width="77" Click="btn_start_Click"/>
<Image x:Name="videoImage" HorizontalAlignment="Left" Height="362" Margin="10,58,0,0" VerticalAlignment="Top" Width="388" Stretch="Fill"/>
<TextBlock x:Name="asciiTextBlock" HorizontalAlignment="Left" Margin="410,58,0,0" VerticalAlignment="Top" Height="366" Width="380" FontSize="3"/>
</Grid>
使用的影像

uj5u.com熱心網友回復:
問題 1: ASCII 藝術未在 WPF 中正確顯示TextBlock:
- ASCII-Art 需要使用固定寬度(又名等寬)字體呈現。
- 您發布的 XAML 顯示您沒有
FontFamily在TextBlock. 使用<TextBlock>的默認值FontFamily,這將是 Segoe UI。- Segoe UI不是等寬字體。
- 問題不在于添加了額外的換行符,而是渲染的線條在視覺上被壓縮了。
- 將您的 XAML 更改為此,它將按預期作業:
- 我還將字體大小從
3增加到10.
- 我還將字體大小從
<TextBlock
x:Name= "asciiTextBlock "
HorizontalAlignment= "Left "
Margin= "410,58,0,0 "
VerticalAlignment= "Top "
Height= "366 "
Width= "380 "
FontSize= "10 "
FontFamily= "Courier New "
/>
截圖證明:
使用默認字體:

與Courier New:

我還制作了一個使用 Unicode 塊字符的版本,只是為了確保我正確解碼了影像:
static readonly Char[] _chars = new[] { ' ', '?', '?', '▓', '█' };

問題 2:性能
使用Bitmap.Lockbits:
- 您可以通過使用
Bitmap.Lockbits而不是迭代來顯著提高性能.GetPixel()。GetPixel()真的很慢:https ://www.codeproject.com/Articles/406045/Why-the-use-of-GetPixel-and-SetPixel-is-so-ineffic
像這樣的東西:
unsafe static String RenderPixelsAsAscii_LockBits( FileInfo imageFile )
{
using( Bitmap bitmap = (Bitmap)System.Drawing.Image.FromFile( imageFile.FullName ) )
{
Rectangle r = new Rectangle( x: 0, y: 0, width: bitmap.Width, height: bitmap.Height );
BitmapData bitmapData = bitmap.LockBits( rect: r, flags: ImageLockMode.ReadOnly, bitmap.PixelFormat );
try
{
StringBuilder sb = new StringBuilder( capacity: bitmapData.Width * bitmapData.Height );
Int32 bytesPerPixel = System.Drawing.Image.GetPixelFormatSize( bitmapData.PixelFormat ) / 8;
Byte* scan0 = (Byte*)bitmapData.Scan0;
for( Int32 y = 0; y < bitmapData.Height; y )
{
Byte* linePtr = scan0 ( y * bitmapData.Stride );
for( Int32 x = 0; x < bitmapData.Width; x )
{
Byte* pixelPtr = linePtr ( x * bytesPerPixel );
UInt32 pixel = *pixelPtr;
sb.AppendPixel( pixel, bitmapData.PixelFormat );
}
sb.Append( '\n' );
}
return sb.ToString();
}
finally
{
bitmap.UnlockBits( bitmapData );
}
}
}
public static void AppendPixel( this StringBuilder sb, UInt32 pixel, PixelFormat fmt )
{
Byte a;
Byte r;
Byte g;
Byte b;
switch( fmt )
{
case PixelFormat.Format24bppRgb:
{
r = (Byte)( ( pixel & 0xFF_00_00_00 ) >> 32 );
g = (Byte)( ( pixel & 0x00_FF_00_00 ) >> 24 );
b = (Byte)( ( pixel & 0x00_00_FF_00 ) >> 16 );
}
break;
case PixelFormat.Format32bppArgb:
{
a = (Byte)( ( pixel & 0xFF_00_00_00 ) >> 24 );
r = (Byte)( ( pixel & 0x00_FF_00_00 ) >> 16 );
g = (Byte)( ( pixel & 0x00_00_FF_00 ) >> 8 );
b = (Byte)( ( pixel & 0x00_00_00_FF ) >> 0 );
}
break;
case PixelFormat.etc...:
// TODO if needed.
default:
throw new NotSupportedException( "meh" );
}
Single avgBrightness = ( (Single)r (Single)g (Single)b ) / 3f;
if ( avgBrightness < 51 ) sb.Append( '█' );
else if( avgBrightness < 102 ) sb.Append( '▓' );
else if( avgBrightness < 153 ) sb.Append( '?' );
else if( avgBrightness < 204 ) sb.Append( '?' );
else sb.Append( ' ' );
}
- Your code can also be modified to be more efficient with
StringBuilder:- Append
charvalues directly instead of converting aCharto aStringwith.ToString(): that's just silly.- Using
StringBuilder.Append(Char)is very fast because it just copies the scalarcharvalue directly intoStringBuilder's internal char buffer. Stringobjects exist on the heap which requires allocation and copying, so they're relatively expensive compared to usingcharvalues, which don't need to use the heap at all until/unless boxed.
- Using
- Additionally, preallocating the
StringBuilderby settingcapacity: width * heightmeans that theStringBuilderwon't need to reallocate and copy its internal buffer every time it overflows.- The default capacity of a
StringBuilderis only 16 chars, so if you can precompute an upperbound for the final length of a StringBuilder for its constructor'scapacity:you should do that.
- The default capacity of a
- Append
Here's the runtime performance figures I get:
- .NET 6 numbers from running in Linqpad 7 (.NET 6) x64 on a PC with a i7-10700K CPU running Windows 10 20H2 x64.
- .NET 4.8 numbers from running in Linqpad 5's AnyCPU build.
- I benchmarked both a
DEBUGand aRELEASEbuild for some reason. - I used
Stopwatchto measure the time taken to convert the already-loadedBitmapto aString- so it doesn't include the time to load the image from disk, nor the time for WPF to render the text, as that's unrelated to my suggested improvements). - The numbers shown are the best of 3 runs after an initial warmup run.
| .NET 6 (RELEASE, x64) | .NET 4.8 (RELEASE, x64) | .NET 6 (DEBUG, x64) | .NET 4.8 (DEBUG, x64) | |
|---|---|---|---|---|
| Your original converter function | 1.41ms | 1.87ms | 2.52ms | 2.90ms |
| Your original converter function, but with improved StringBuilder usage | 1.37ms | 1.76ms | 2.41ms | 2.85ms |
| Using LockBits instead | 0.04ms | 0.04ms | 0.43ms | 0.13ms |
Relative performance improvement of LockBits |
~35x | ~46x | ~6x | ~22x |
- The
0.04msfigure is not a typo. It really is that fast. - The improved
StringBuilderusage does help, but I'll agree that the sub-millisecond times shaved off really isn't significant. - I appreciate that for this project on modern hardware, even the worst-case of 2.90ms isn't bad for the
GetPixelapproach but I was really surprised at how fast the "slow" approach is now....- ...compared to about 17 years ago when I was first learning .NET and wanting to use
System.Drawingto create a dynamic image-macro1 generator for my website and attempting to read even a 500x500pxBitmapon my then single-core Pentium 4 1.9Ghz (not even Hyper-Threading) took at least a few seconds, which led me to seek-out a faster way, after all, even my old Pentium 166 could process 640x480-sized bitmap images from ancient video file formats in real-time, so I assumed I was doing something wrong.
- ...compared to about 17 years ago when I was first learning .NET and wanting to use
Here's my code, you should be able to copy paste this into Linqpad or a new blank C# project:
const String XAML_TEXT = @"
<Window
xmlns =""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x =""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:mc =""http://schemas.openxmlformats.org/markup-compatibility/2006""
xmlns:local =""clr-namespace:AsciiImageWPF""
Title=""MainWindow""
Width = ""1024""
Height = ""1024""
>
<Grid ShowGridLines=""True"">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock
Grid.Row=""0""
Grid.Column=""0""
Text=""Variable-width font, your chars""
/>
<TextBlock
Grid.Row=""0""
Grid.Column=""1""
Text=""Variable-width font, block chars""
/>
<TextBlock
Grid.Row=""1""
Grid.Column=""0""
Text=""Monospace font, your chars""
/>
<TextBlock
Grid.Row=""1""
Grid.Column=""1""
Text=""Monospace font, block chars""
/>
<!-- ############################# -->
<TextBlock
x:Name=""asciiTextBlockVariableSlow""
Grid.Row=""0""
Grid.Column=""0""
Margin=""0,20,0,0""
FontSize=""8""
Background=""#f2fff2""
/>
<TextBlock
x:Name=""asciiTextBlockVariableFast""
Grid.Row=""0""
Grid.Column=""1""
Margin=""0,20,0,0""
FontSize=""8""
Background=""#ffeeed""
/>
<TextBlock
x:Name=""asciiTextBlockMonospaceSlow""
Grid.Row=""1""
Grid.Column=""0""
Margin=""0,20,0,0""
FontSize=""8""
Background=""#f2f7ff""
FontFamily=""Consolas""
/>
<TextBlock
x:Name=""asciiTextBlockMonospaceFast""
Grid.Row=""1""
Grid.Column=""1""
Margin=""0,20,0,0""
FontSize=""8""
Background=""#fffff2""
FontFamily=""Consolas""
/>
</Grid>
</Window>
";
const String IMAGE_PATH = @"C:\Users\YOU\Downloads\2022-03\xfqJC.png";
async Task Main()
{
StringReader stringReader = new StringReader( XAML_TEXT );
XmlReader xmlReader = XmlReader.Create( stringReader );
Window window = (Window)XamlReader.Load( xmlReader );
window.Show();
///////
FileInfo imageFile = new FileInfo( IMAGE_PATH );
String orig = RenderPixelsAsAscii_Orig( imageFile );
String orig2 = RenderPixelsAsAscii_Orig_better_StringBuilder( imageFile );
String mine = RenderPixelsAsAscii_LockBits( imageFile );
//
TextBlock asciiTextBlockVariableSlow = (TextBlock)window.FindName( name: "asciiTextBlockVariableSlow" );
TextBlock asciiTextBlockVariableFast = (TextBlock)window.FindName( name: "asciiTextBlockVariableFast" );
TextBlock asciiTextBlockMonospaceSlow = (TextBlock)window.FindName( name: "asciiTextBlockMonospaceSlow" );
TextBlock asciiTextBlockMonospaceFast = (TextBlock)window.FindName( name: "asciiTextBlockMonospaceFast" );
window.Dispatcher.Invoke( () => {
asciiTextBlockVariableSlow.Text = orig;
asciiTextBlockVariableFast.Text = mine;
asciiTextBlockMonospaceSlow.Text = orig;
asciiTextBlockMonospaceFast.Text = mine;
} );
}
static string chars = " .:-^$@";
static String RenderPixelsAsAscii_Orig( FileInfo imageFile )
{
Stopwatch sw = Stopwatch.StartNew();
using( Bitmap image = (Bitmap)System.Drawing.Image.FromFile( imageFile.FullName ) )
{
// sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_Orig: Time to load image from disk." );
sw.Restart();
StringBuilder sb = new StringBuilder();
for (int j = 0; j < image.Height; j )
{
for (int i = 0; i < image.Width; i )
{
Color pixelColor = image.GetPixel(i,j);
int brightness = (pixelColor.R pixelColor.G pixelColor.B) / 3;
int charIndex = brightness.Remap(0, 255, 0, chars.Length - 1);
string c = chars[charIndex].ToString();
sb.Append(c);
}
sb.Append("\n");
}
sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_Orig: Time to render bitmap as ASCII." );
//asciiTextBlock.Text = sb.ToString();
return sb.ToString();
}
}
static String RenderPixelsAsAscii_Orig_better_StringBuilder( FileInfo imageFile )
{
Stopwatch sw = Stopwatch.StartNew();
using( Bitmap image = (Bitmap)System.Drawing.Image.FromFile( imageFile.FullName ) )
{
// sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_Orig_better_StringBuilder: Time to load image from disk." );
sw.Restart();
StringBuilder sb = new StringBuilder( capacity: image.Width * image.Height );
for (int j = 0; j < image.Height; j )
{
for (int i = 0; i < image.Width; i )
{
Color pixelColor = image.GetPixel(i,j);
int brightness = (pixelColor.R pixelColor.G pixelColor.B) / 3;
int charIndex = brightness.Remap(0, 255, 0, chars.Length - 1);
sb.Append(chars[charIndex]);
}
sb.Append('\n');
}
sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_Orig_better_StringBuilder: Time to render bitmap as ASCII." );
//asciiTextBlock.Text = sb.ToString();
return sb.ToString();
}
}
unsafe static String RenderPixelsAsAscii_LockBits( FileInfo imageFile )
{
Stopwatch sw = Stopwatch.StartNew();
using( Bitmap bitmap = (Bitmap)System.Drawing.Image.FromFile( imageFile.FullName ) )
{
// sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_LockBits: Time to load image from disk." );
sw.Restart();
Rectangle r = new Rectangle( x: 0, y: 0, width: bitmap.Width, height: bitmap.Height );
BitmapData bitmapData = bitmap.LockBits( rect: r, flags: ImageLockMode.ReadOnly, bitmap.PixelFormat );
try
{
StringBuilder sb = new StringBuilder( capacity: bitmapData.Width * bitmapData.Height );
RenderPixelsAsAsciiInner( bitmapData, sb );
sw.Elapsed.TotalMilliseconds.Dump( "RenderPixelsAsAscii_LockBits: Time to render bitmap as ASCII." );
return sb.ToString();
}
finally
{
bitmap.UnlockBits( bitmapData );
}
}
}
unsafe static void RenderPixelsAsAsciiInner( BitmapData bitmapData, StringBuilder sb )
{
// bpp == Length of each pixel in bytes. e.g. 24-bit RGB is 3, and 32-bit ARGB is 4.
Int32 bitsPerPixel = System.Drawing.Image.GetPixelFormatSize( bitmapData.PixelFormat );
if( ( bitsPerPixel % 8 ) != 0 ) throw new NotSupportedException( "Image uses a non-integral-pixel-byte-width format: " bitmapData.PixelFormat );
Int32 bytesPerPixel = bitsPerPixel / 8;
Byte* scan0 = (Byte*)bitmapData.Scan0;
for( Int32 y = 0; y < bitmapData.Height; y )
{
Byte* linePtr = scan0 ( y * bitmapData.Stride );
for( Int32 x = 0; x < bitmapData.Width; x )
{
Byte* pixelPtr = linePtr ( x * bytesPerPixel );
UInt32 pixel = *pixelPtr;
sb.AppendPixel( pixel, bitmapData.PixelFormat );
}
sb.Append( '\n' );
}
}
static class MyExtensions
{
static readonly Char[] _chars = new[] { ' ', '?', '?', '▓', '█' }; // " .:-^$@";
public static void AppendPixel( this StringBuilder sb, UInt32 pixel, PixelFormat fmt )
{
Byte a;
Byte r;
Byte g;
Byte b;
switch( fmt )
{
case PixelFormat.Format24bppRgb:
{
r = (Byte)( ( pixel & 0xFF_00_00_00 ) >> 24 );
g = (Byte)( ( pixel & 0x00_FF_00_00 ) >> 16 );
b = (Byte)( ( pixel & 0x00_00_FF_00 ) >> 8 );
}
break;
case PixelFormat.Format32bppArgb:
{
a = (Byte)( ( pixel & 0xFF_00_00_00 ) >> 24 );
r = (Byte)( ( pixel & 0x00_FF_00_00 ) >> 16 );
g = (Byte)( ( pixel & 0x00_00_FF_00 ) >> 8 );
b = (Byte)( ( pixel & 0x00_00_00_FF ) >> 0 );
}
break;
default:
throw new NotSupportedException( "meh" );
}
Single avgBrightness = ( (Single)r (Single)g (Single)b ) / 3f;
if ( avgBrightness < 51 ) sb.Append( '█' );
else if( avgBrightness < 102 ) sb.Append( '▓' );
else if( avgBrightness < 153 ) sb.Append( '?' );
else if( avgBrightness < 204 ) sb.Append( '?' );
else sb.Append( ' ' );
}
public static int Remap( this int value, int from1, int to1, int from2, int to2)
{
return (value - from1) / (to1 - from1) * (to2 - from2) from2;
}
}
1 Now they're just called memes. This was before Facebook, before the time when the Internet went mainstream, now it's just lame yaknow?
Screenshot proof:
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/437172.html
上一篇:如何系結到集合內的集合?
