画像をクリックした場所に文字列を表示する

画像に文字列をオーバレイ描画する方法を紹介します。例えばこういったシチュエーションが考えられます。

  • カメラで画像をキャプチャしているときに、画面の隅に赤い文字で REC と描画したい。
  • 動画のスタートからの経過時間を 12:34:56.789 のように常に表示させたい。
  • セマンティックセグメンテーションによるオブジェクト抽出結果を表示させたい。man とか road とか car とか。
  • ブロッブ解析結果の画素数や外接矩形の縦横サイズをブロッブの近くに表示させたい。

下記に示すコードは、画像をマウスでクリックした場所の座標を、マウスでクリックした場所に文字列オーバレイ描画します。

<Window x:Class="aaa.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:aaa"
        mc:Ignorable="d"
        Title="MainWindow" Width="720" Height="760">

    <DockPanel>

        <Menu DockPanel.Dock="Top">

            <MenuItem x:Name="menuApplication" Header="Application">
                <MenuItem x:Name="menuApplicationQuit" Header="Quit" Click="menuApplicationQuit_Click"/>
            </MenuItem>

        </Menu>

        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button x:Name="tbnQuit" Content="Quit" Width ="48" Height="48" Click="menuApplicationQuit_Click"></Button>
            </ToolBar>
        </ToolBarTray>

        <StatusBar DockPanel.Dock="Bottom" Height="30">
            <Label Name="stbLabel000"/>
            <Separator/>
            <Label Name="stbLabel001"/>
            <Separator/>
            <Label Name="stbLabel002"/>
        </StatusBar>

        <Border BorderBrush="DarkViolet" BorderThickness="1" >
            <ScrollViewer Name="scv000" Background="DarkViolet" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible">
                <Grid x:Name="grid000" MouseDown="OnMouseDown" MouseMove="OnMouseMove">

                    <Image x:Name="image000" Stretch="Uniform"/>

                    <Canvas x:Name="canvas000" Background="Transparent">
                        <Line x:Name="cross_hair_horz" Stroke="Aqua" StrokeThickness="2"/>
                        <Line x:Name="cross_hair_vert" Stroke="Aqua" StrokeThickness="2"/>
                        <TextBlock x:Name="tbk_pos" Foreground="Orange" Background="Transparent" FontSize="16pt" Text="default_string"/>
                    </Canvas>
                    
                </Grid>
            </ScrollViewer>
        </Border>

    </DockPanel>


</Window>

上記の xaml で、tbk_pos という名前のテキストブロックを定義しておきます。(上記コードの43行目)

下記の xaml.cs で、そのテキストブロックが保持する文字列の内容や、テキストブロックの位置を制御してやります。

228行目、テキストブロックに表示したい文字列を仕込みます。
229行目、テキストブロックの左右位置を指定します。Canvas.SetLeft();
230行目、テキストブロックの上下位置を指定します。Canvas.SetTop();

using System.Windows.Media.Imaging; をお忘れなく.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media;

// これの追加を忘れないように. Don't forget to add this sentence.
using System.Windows.Media.Imaging;

namespace aaa
{

	public partial class MainWindow : Window
	{

		const double DPI_X = 96.0;
		const double DPI_Y = 96.0;

		byte [] Data000;
		WriteableBitmap Bmp000;

		int TheX;
		int TheY;

		// 画像のサイズ.
		const int REQ_W = 512;
		const int REQ_H = 512;

		// コサイン波形の周期.
		const double CYCLE_HORZ = 8.0;
		const double CYCLE_VERT = 4.0;

		// 水平垂直加算コサインデータを取得する.
		private bool get_data_cos_wave_horz_vert(
						byte [] data,
						int width,
						int height,
						double cycle_horz, // 水平方向の周期.
						double cycle_vert  // 垂直方向の周期.
						)
		{

			int w = width;
			int h = height;

			double two_pai = 2.0 * Math.PI;
			double rate;
			double rad;

			double tmp;

			double [] table_for_a = new double[ h ];
			double [] table_for_b = new double[ w ];

			const double OFFSET = 1.0;
			const double MUL = 0.5;

			// テーブルを作る.
			for ( int j = 0; j < h; j++ )
			{
				// ラジアンを算出する.
				rate = (double)(j)/(double)( h - 1 );
				rad = rate * two_pai;

				// ここでコサインの計算をする.
				tmp = -( Math.Cos( cycle_vert * rad ));

				// 0.0 から 1.0 が格納される.
				table_for_a[j] = (( tmp + OFFSET ) * MUL );
			}

			// テーブルを作る.
			for ( int i = 0; i < w; i++ )
			{
				// ラジアンを算出する.
				rate = (double)(i)/(double)( w - 1 );
				rad = rate * two_pai;

				// ここでコサインの計算をする.
				tmp = -( Math.Cos( cycle_horz * rad ));

				// 0.0 から 1.0 が格納される.
				table_for_b[i] = (( tmp + OFFSET ) * MUL );
			}

			double a;
			double b;
			byte level;

			int adrs;

			for ( int j = 0; j < h; j++ )
			{

				// 0.0 から 1.0 の範囲をとる.
				a = table_for_a[j];

				adrs = w * j;

				for ( int i = 0; i < w; i++ )
				{
					// 0.0 から 1.0 の範囲をとる.
					b = table_for_b[i];

					// 両方を加算すると 0.0 から 2.0 の範囲をとる.
					tmp = a + b; 

					// 0 から 255 の byte 区間にマッピングする.
					level = (byte)( Math.Round(( tmp * 0.5 ) * 255.0 ));

					// 8bits画像データとして格納する.
					data[ adrs ] = level;

					adrs++;

				}
			}

			return true;

		}

		///////////////////////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////

		public MainWindow()
		{

			// これはデフォルトのコード.
			InitializeComponent();

			// データ領域を確保する.
			int w = REQ_W;
			int h = REQ_H;
			int numpix = w * h;
			Data000 = new byte[ numpix ];

			// 確保した領域にデータを仕込む.
			get_data_cos_wave_horz_vert( Data000, w, h, CYCLE_HORZ, CYCLE_VERT );

			// グレースケールパレットの要素数.
			const int NUM_ELEMENT_OF_PAL = 256;

			// 8bppのビットマップを作るので256要素カラーパレットを作る.
			List<Color> list_color = new List<Color>(0);
			for ( int k = 0; k < NUM_ELEMENT_OF_PAL; k++ )
			{
				const byte LEVEL_ALPHA = 0xff;
				byte value = ( byte )(k);
				list_color.Add( Color.FromArgb( LEVEL_ALPHA, value, value, value ) );
			}

			// 白黒グレースケールのカラーパレット.
			BitmapPalette Palette = new BitmapPalette( list_color );

			// 8bppのビットマップを生成する.
			PixelFormat pixfmt = PixelFormats.Indexed8;
			Bmp000 = new WriteableBitmap( w, h, DPI_X, DPI_Y, pixfmt, Palette );

			// -------------------------------------------------------------------
			// Bmp000 に WritePixels( Data000 ) するのは OnRender() で実施する.
			// -------------------------------------------------------------------

			// 画像出力先のサイズを決定する.
			image000.Width = Bmp000.PixelWidth;
			image000.Height = Bmp000.PixelHeight;

			// 画像出力先にビットマップを関係づける.
			image000.Source = Bmp000;

			// 十字カーソルはまんなかに出現.
			TheX = w >> 1;
			TheY = h >> 1;

			// ウインドウのタイトル.
			this.Title = "GazoYaro";

		}

		protected override void OnRender( DrawingContext dc )
		{

			// 画像のサイズを取得する.
			int w = Bmp000.PixelWidth;
			int h = Bmp000.PixelHeight;

			// ビットマップに画像データを書き込む.
			Int32Rect rct = new Int32Rect( 0, 0, w, h );
			int stride_bytes = w * sizeof( byte );
			int offset_bytes = 0;
			Bmp000.WritePixels( rct, Data000, stride_bytes, offset_bytes );

			// 親要素 Grid000 の座標系から image000 のオフセット量を取得する.
			Point img_point = image000.TranslatePoint( new Point( 0, 0 ), grid000 );
			double plot_offset_x = img_point.X;
			double plot_offset_y = img_point.Y;

			// Canvas の座標系は Grid の座標系と同じ.
			double plot_x = (double)( TheX + plot_offset_x );
			double plot_y = (double)( TheY + plot_offset_y );

			// 出力コントロール image000 のサイズを取得する.
			double img_w = image000.Width;
			double img_h = image000.Height;

			// 水平線を引く(十字線の横).
			cross_hair_vert.X1 = plot_x;
			cross_hair_vert.Y1 = plot_offset_y;
			cross_hair_vert.X2 = plot_x;
			cross_hair_vert.Y2 = plot_offset_y + img_h;

			// 垂直線を引く(十字線の縦).
			cross_hair_horz.X1 = plot_offset_x;
			cross_hair_horz.Y1 = plot_y;
			cross_hair_horz.X2 = plot_offset_x + img_w;
			cross_hair_horz.Y2 = plot_y;

			// テキストブロックの文字列を設定して位置を指定する.
			tbk_pos.Text = String.Format( "({0},{1})", TheX, TheY );
			Canvas.SetLeft( tbk_pos, plot_x );
			Canvas.SetTop( tbk_pos, plot_y );

			// ステータスバーのラベルに座標を表示する.
			const String FMT000 = "BmpWH {0}*{1}";
			const String FMT001 = "PlotOffsetXY({0},{1})";;
			const String FMT002 = "TheXY({0},{1})";
			String str000 = String.Format( FMT000, Bmp000.PixelWidth, Bmp000.PixelHeight );
			String str001 = String.Format( FMT001, plot_offset_x, plot_offset_y );
			String str002 = String.Format( FMT002, TheX, TheY );
			stbLabel000.Content = str000;
			stbLabel001.Content = str001;
			stbLabel002.Content = str002;

		}

		private void OnMouseDown(object sender, MouseEventArgs e)
		{

			// 画像のサイズを取得する.
			int w = (int)( Math.Round( image000.Width ) );
			int h = (int)( Math.Round( image000.Height ) );

			// image000 基準のマウス座標を取得する.
			Point pnt = e.GetPosition( image000 );

			// ここに入る値はゼロ未満や幅高以上もありうる.
			int tmp_x = (int)( Math.Round( pnt.X ) );
			int tmp_y = (int)( Math.Round( pnt.Y ) );

			// クリックした座標が画像からはみ出ないようにする.
			if ( tmp_x < 0 ) { tmp_x = 0; }
			if ( tmp_y < 0 ) { tmp_y = 0; }
			if ( tmp_x >= w ) { tmp_x = w - 1; }
			if ( tmp_y >= h ) { tmp_y = h - 1; }

			// ここで代入される値は画像領域の内側であることが保証される.
			TheX = tmp_x;
			TheY = tmp_y;

			// 再描画をうながす、OnRender() が呼ばれる.
			this.InvalidateVisual();

		}

		private void OnMouseMove(object sender, MouseEventArgs e)
		{

			// 左ボタンが押し下げられているかどうか.
			if (( e.LeftButton & MouseButtonState.Pressed ) == MouseButtonState.Pressed )
			{
				// 押し下げられていたら OnMouseDown を実行する.
				this.OnMouseDown( sender, e );
			}

		}

		private void menuApplicationQuit_Click( object sender, RoutedEventArgs e )
		{

			// ウインドウを閉じてアプリケーションを終了する.
			this.Close();

		}

	}
}

サンプルソースを用意しました。上記のコードのコピーペーストでうまく動かない場合にご利用ください。