Edited at

【C#/WPF】WriteableBitmapで点を打って、テレビの放送終了後の砂嵐をつくる(タイマーで処理)


やりたいこと

任意の場所に点を打って、画像を作成したい。


WriteableBitmapを使う

WriteableBitmapは、BitmapSourceクラスから派生したクラス。

他にBitmapSourceから派生したクラスに、BitmapImage、RenderTargetBitmap、TransformedBitmapなどがある。(msdocs/自分のページ)

それぞれ派生クラスには役割があるが、WriteableBitmapは、ピクセル単位で任意の点を打つことができる。

想像するに、写真やwebカメラ画像の上にWriteableBitmapで書いた透明の領域と半透明の黒い領域をを含む画像をかぶせて、「無効領域」などを表現したりできそう。


試しに作成したサンプル(ViewModelのみ抜粋)

試しに、掲題の「砂嵐」を出すサンプルを作成した。

using System;

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace WpfApp1
{
class ViewModel : BindingBase
{
public WriteableBitmap MyWBitmap { get; set; } = new WriteableBitmap(1280, 720, 96, 96, PixelFormats.Pbgra32, null);

DispatcherTimer _drawingTimer = new DispatcherTimer();

public ViewModel()
{
// 描画タイマー作動
_drawingTimer.Interval = new TimeSpan(0, 0, 0, 0, 33);
_drawingTimer.Tick += _timer_draw_Tick;
_drawingTimer.Start();
}

// 画面の更新はメインスレッドで実施しないといけないので、Task等ではできない。
// (具体的には、MyWBitmapに値を入れるところを別スレッドにはできない)
private void _timer_draw_Tick(object sender, EventArgs e)
{
int width = (int)1280;
int height = (int)720;

// 計算用のバイト列の準備
int pixelsSize = (int)(width * height * 4);
byte[] pixels = new byte[pixelsSize];

// 乱数の準備
Random rnd = new System.Random();
int rndMax = 256; // 0~256の乱数を取得

// バイト列に色情報を入れる
for (int i = 0; i < width * height; i++)
{
pixels[4 * i] = (byte)rnd.Next(rndMax); //blue;
pixels[4 * i + 1] = (byte)rnd.Next(rndMax); // green;
pixels[4 * i + 2] = (byte)rnd.Next(rndMax); // red;
pixels[4 * i + 3] = (byte)255; //alpha
}

// バイト列をBitmapImageに変換する
int stride = width * 4; // 一行あたりのバイトサイズ(Pbgra32だから、1ピクセル当たり4バイトとなるので、ピクセル数に4を掛ける)
MyWBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0, 0);
}
}
}


ViewModelの概要


  • コードの「// バイト列に色情報を入れる」のところで、ピクセルのデータを作成している。forループ1回分が、1ピクセルのイメージ。

  • 1ピクセル当たり4バイトを使っていて、それぞれ前から[青][緑][赤][アルファ]を示す。

  • なので、透明の領域を作りたい場合は、各ピクセルの4バイト目のアルファ値を0にすればよい。

  • ((未検証)各ピクセルが4バイトなのは、PixelFormats.Pbgra32の場合で、ほかの形式を指定した場合は1ピクセル当たりのバイト数が変わるっぽい(こちら参照)

  • 作ったバイト列を作ったWriteableBitmapのWritePixelsメソッドに渡せば、画像が更新される。


  • 砂嵐は、byte列に値を入れるときに、乱数を入れるだけで作れる。


Xaml側

<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Viewbox Grid.Row="0" Grid.Column="1">
<Image Source="{Binding MyWBitmap}"/>
</Viewbox>
</Grid>
</Window>


動かした画面

image.png


unsafeなし

ここでは、byte配列を普通に作って、それをWritePixelsに渡す形で実現したのでunsafeは出てこなかったが、unsafeにして、byteのポインタを使って実現する方法もあるらしい。

C++のライブラリで作成された画像データのbyte配列をC#で受け取るときにそういうことをしているのを見たことがある。

参考:https://blogs.yahoo.co.jp/gogowaten/15497771.html


Taskで処理したバージョン

こちら

Taskは別スレッドで処理が行われるので、Taskの中でも、UI関連の処理をするときだけUIスレッドに戻すということをしないといけない。


コード

https://github.com/tera1707/WPF-/tree/master/022_WriteableBitmap


参考

WriteableBitmap Class

https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.imaging.writeablebitmap?view=netframework-4.8

PixelFormats Class

https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.pixelformats?view=netframework-4.8

C#で画像を描いてみた(WPFでWritableBitmap編)

https://water2litter.net/gin/?p=984