オフスクリーンバッファでビットマップをピクチャボックスに表示する

カメラの画像などをリアルタイムにピクチャボックスに表示する場合、画像がフリッカする場合が多いです。特に高速フレームレートのカメラを利用している場合は特にそうです。

C# では Form を DoubleBuffer プロパティを true にするという方法がありますが、それでは防げない場合が多いです。

意図的にフリッカを発生させるコードは難しいですが、スレッドを起動しスレッドワーカーにビットマップ画像を更新させてウインドウ側で描画させれば、ときどきフリッカを発生させることができました。これで投稿の件名にあるオフスクリーンバッファを体験してみましょう。

フォームデザイナで Form1 に button1, button2, button3 と timer1 と pictureBox1 を配置してください。プロパティエディタを使ったり、デザイン画面でダブルクリックしたりして

Form1 に Load イベント
Form1 に OnClosing イベント
button1 に Click イベント
button2 に Click イベント
button3 に Click イベント
timer1 に Tick イベント
picureBox1 に Paint イベント

を定義してください。

ウインドウ画面は、真っ黒から真っ青に徐々に変化していき、真っ青になったら真っ黒に戻ります。この遷移中にフリッカが出るときもあります。

下記のコードでオフスクリーンバッファの効き目が実感できるかもしれません。高速なパソコンだとフリッカを再現できないかもです。MFCの時代を思い出しますね。

using System;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;

using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;

namespace aaa
{
	public partial class Form1 : Form
	{

		// スレッドに渡すパラメータとスレッド.
		// このコードの下のほうで定義している.
		ParametersForThread Pft;
		Thread ThreadCounterInc = null;

		// オフスクリーンバッファをつかうかどうか.
		bool FlagUseOffscreenBuffer = true;

		public Form1()
		{

			InitializeComponent();

			// 画面のクライアント領域の幅高サイズ.
			int req_w = this.ClientRectangle.Width;
			int req_h = this.ClientRectangle.Height;

			// 色レベルをカウントアップするスレッドを起動する.
			Pft = new ParametersForThread( req_w, req_h );
			ParameterizedThreadStart pts = new ParameterizedThreadStart( Worker_CounterInc );
			ThreadCounterInc = new Thread( pts );
			ThreadCounterInc.Start( Pft );

			// ピクチャボックスの位置とサイズを決める.
			pictureBox1.Location = new Point( 0, 0 );
			pictureBox1.Size = new Size( req_w, req_h );

			// OffScreenBuffer の略 OSB.
			button1.Text = "OSB true";
			button2.Text = "OSB false";
			button3.Text = "Close";

		}

		private void Form1_Load( object sender, EventArgs e )
		{
			// タイマの間隔を短くするとフリッカがでやすい.
			timer1.Interval = 1;
			timer1.Start();
		}

		private void Form1_FormClosing( object sender, FormClosingEventArgs e )
		{

			// ウインドウが閉じるときスレッド終了を要請する.
			Pft.FlagThreadExit = true;

			// スレッド終了までちょっと待つ.
			Thread.Sleep( 100 );

			// スレッド終了待ちをする.
			ThreadCounterInc.Join();

		}

		private void button1_Click( object sender, EventArgs e )
		{
			// オフスクリーンバッファ使う.
			FlagUseOffscreenBuffer = true;
		}

		private void button2_Click( object sender, EventArgs e )
		{
			// オフスクリーンバッファ使わない.
			FlagUseOffscreenBuffer = false;
		}

		private void button3_Click( object sender, EventArgs e )
		{
			// ウインドウを閉じる.
			this.Close();
		}

		private void timer1_Tick( object sender, EventArgs e )
		{
			// ピクチャボックスの再描画を要請する.
			pictureBox1.Invalidate( true );
		}

		private void pictureBox1_Paint( object sender, PaintEventArgs e )
		{

			Stopwatch stopwatch = new Stopwatch();
			stopwatch.Start();

			if ( FlagUseOffscreenBuffer )
			{

				// オフスクリーンバッファを使うとき.
				if ( Pft != null )
				{
					if ( Pft.Bmp != null )
					{
						// スレッド側でビットマップを操作していなければ下記のブロックに突入する.
						if ( Interlocked.CompareExchange( ref ( Pft.FlagDontTouchBmp ), 0, 0 ) == 0 )
						{
							// ビットマップ利用中のフラグをたてる.
							Interlocked.Exchange( ref( Pft.FlagNowUseBmp ), 1 );

							// ピクチャボックスの領域.
							Rectangle rct = pictureBox1.ClientRectangle;

							// オフスクリーンバッファに描画する.
							using ( BufferedGraphics off_screen_buffer = BufferedGraphicsManager.Current.Allocate( e.Graphics, rct ) )
							{
								// いったんオフスクリーンバッファを塗りつぶして初期化する.
								off_screen_buffer.Graphics.Clear( Color.Transparent );

								// オフスクリーンバッファにビットマップを描画する.
								int w = Pft.BmpW;
								int h = Pft.BmpH;
								off_screen_buffer.Graphics.DrawImage( Pft.Bmp, 0, 0, w, h );

								// ここでオフスクリーンバッファをピクチャボックスに転送する.
								off_screen_buffer.Render( e.Graphics );
							}

							// ビットマップ利用中のフラグをおろす.
							Interlocked.Exchange( ref( Pft.FlagNowUseBmp ), 0 );
						}
					}
				}

			}
			else
			{

				// こちらはティアリングが出がちな素朴なコード.
				if ( Pft != null )
				{
					if ( Pft.Bmp != null )
					{
						// スレッド側でビットマップを操作していなければ下記のブロックに突入する.
						if ( Interlocked.CompareExchange( ref ( Pft.FlagDontTouchBmp ), 0, 0 ) == 0 )
						{
							// ビットマップ利用中のフラグをたてる.
							Interlocked.Exchange( ref( Pft.FlagNowUseBmp ), 1 );

							// ピクチャボックスにビットマップを描画する.
							int w = Pft.BmpW;
							int h = Pft.BmpH;
							e.Graphics.DrawImage( Pft.Bmp, 0, 0, w, h );

							// ビットマップ利用中のフラグをおろす.
							Interlocked.Exchange( ref( Pft.FlagNowUseBmp ), 0 );
						}
					}
				}

			}

			// タイトルバーに情報を出す.
			this.Text = $"FlagUseOffScreenBuffer is {FlagUseOffscreenBuffer}, {stopwatch.ElapsedMilliseconds}msec.";

		}

		// ここからスレッド関係のコード.

		// スレッドワーカー.
		public void Worker_CounterInc( object parameter )
		{

			ParametersForThread prm = (ParametersForThread)( parameter );

			for (;;)
			{
			
				if ( prm.FlagThreadExit )
				{
					prm.FlagThreadExit = false;
					return;
				}

				// 青成分のみカウンタで増やす.
				Color c = Color.FromArgb( 0, 0, Pft.Counter );

				// ------------------------------------------------------------------------
				// ビットマップ参照禁止フラグをたてる.
				Interlocked.Exchange( ref( prm.FlagDontTouchBmp ), 1 );

				// 別の場所でビットマップをつかっていなければ色変更を実施する.
				if ( Interlocked.CompareExchange( ref( prm.FlagNowUseBmp ), 0, 0 ) == 0 )
				{
					// ここで色を変更する.
					using ( Graphics grph = Graphics.FromImage( prm.Bmp ) )
					{
						grph.Clear( c );
					}
				}

				// ビットマップ参照禁止フラグをおろす.
				Interlocked.Exchange( ref( prm.FlagDontTouchBmp ), 0 );
				// ------------------------------------------------------------------------

				( prm.Counter )++;

				if ( prm.Counter > 255 )
				{
					prm.Counter = 0;
				}

				// UIスレッドでPictureBoxに反映(コメントアウトしています).
				// タイマイベントを使わない場合これを活かす.
				// こっちなら確実にフリッカしない.
				//this.Invoke(( MethodInvoker ) delegate { pictureBox1.Refresh(); } );

				// ここを Sleep(0) にするとずっと色レベル変更をしている状態とほとんど同じ.
				// したがってピクチャボックスに描画権が与えられない場合が多い.
				// イベント処理も通らない場合が多い.
				Thread.Sleep(1);

			}

		}

		// スレッドワーカに渡すパラメータクラスの定義.
		public class ParametersForThread
		{

			public bool FlagThreadExit;

			public int Counter;
			public Bitmap Bmp;
			public int FlagDontTouchBmp;
			public int FlagNowUseBmp;

			public int BmpW;
			public int BmpH;

			public ParametersForThread( int bmp_width, int bmp_height )
			{
				Counter = 0;
				FlagThreadExit = false;
				FlagDontTouchBmp = 0;
				FlagNowUseBmp = 0;

				PixelFormat pixfmt = PixelFormat.Format24bppRgb;
				Bmp = new Bitmap( bmp_width, bmp_height, pixfmt );

				BmpW = bmp_width;
				BmpH = bmp_height;
			}

		}
	}
}

最後に余談です、MFC の時代では、

デバイスコンテキスト(DC)を取得して
 →DCに関連付けた互換オフスクリーンビットマップを作成して
  →オフスクリーンビットマップに描画
   →オフスクリーンビットマップをDCに描画
    →オフスクリーンビットマップを破棄
     →DCをリリース

ということをしておりましたが、そのやり方と同じだと思います。ダブルバッファで足りないときはトリプルバッファとかもやってましたね。

ダブルバッファで常に描画時間に間に合わないときは、トリプルバッファにしても意味ないんですけど。MFC時代はなんか自己満でやってました(笑)。