CreateBitmapSourceFromHBitmapでメモリリークが発生した件
先日よりC#で画面スクリーンショット方法をまとめていました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com
そこで、CreateBitmapSourceFromHBitmap関数でどうもメモリリークが発生してしまい、
メモリ使用量がどんどん増えていく。。
ということで原因と対応策を調べたので備忘録としてまとめておくことしました。
※同じミスはしたくないので。
■原因
メモリリークが発生していたコードですが、こちらのような実装をしておりました。
using (var bmtpScreen = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpFromImg = Graphics.FromImage(bmtpScreen)) { bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size); Imaging.CreateBitmapSourceFromHBitmap( bmtpScreen.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } }
こちらのコードですと、
CreateBitmapSourceFromHBitmap実行を繰り返す度にメモリリークが発生してしまい、
困ったことになりました。
そもそもCreateBitmapSourceFromHBitmap関数の引数ですが、
・第1引数:アンマネージ ビットマップへのポインター(IntPtr)
・第2引数:ビットマップのパレットマップへのポインター(IntPtr)
・第3引数:ソースイメージのサイズ(Int32Rect)
・第4引数:変換を処理する方法を指定する列挙体の値(BitmapSizeOptions)
となります。
※参考:https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.interop.imaging.createbitmapsourcefromhbitmap?view=windowsdesktop-6.0
自分で作成したコードですが、
return Imaging.CreateBitmapSourceFromHBitmap(
bmtpScreen.GetHbitmap(),
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
でしたので、第1引数のbmtpScreen.GetHbitmap()が怪しそう。。
ということで調べてみたところ、
第1引数に与えている bmtpScreen.GetHbitmap()は明示的に開放しないとメモリリークにつながることを発見。
fkmt5.hatenadiary.org
自分のコードだとメモリ開放はしていないので、
メモリが開放されず蓄積してしまいメモリリークにつながってしまっていたようです。
■解決方法
bmtpScreen.GetHbitmap()のメモリを解放すればよいことが分かりましたので、実際に開放処理を追加すればOK。
開放にはDeleteObject()を利用することにしました。
DeleteObject()とは、、、
WIn32 APIの1つで、
論理ペン、ブラシ、フォント、ビットマップ、領域、またはパレットを削除し、オブジェクトに関連付けられているすべてのシステムリソースを解放
する関数になります。
オブジェクトが削除されると、指定されたハンドルは無効になります。
※参考:https://docs.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-deleteobject
DeleteObject()を利用する場合にはあらかじめ、
[System.Runtime.InteropServices.DllImport("gdi32.dll")] public static extern bool DeleteObject(IntPtr hObject);
と定義すればOK。
後は、
using (var bmtpScreen = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpFromImg = Graphics.FromImage(bmtpScreen)) { bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size); var ptrBuf = bmtpScreen.GetHbitmap(); Imaging.CreateBitmapSourceFromHBitmap( ptrBuf , IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); DeleteObject(ptrBuf); } }
といったように、
DeleteObjectで、
var ptrBuf = bmtpScreen.GetHbitmap()
で定義したリソースを明示的に開放してあげればOKです。
■最後に
今回は自分が躓いたメモリリークに関してまとめておきました。
次回同じようなコードを書く時には気を付けたいと思います。
WPFで自由にRectangleを描画する
先日、WPFで特定のアプリケーションに限定してスリーンショット録画の方法をまとめました。
elsammit-beginnerblg.hatenablog.com
ですが、
・複数アプリを録画したい
・好きなエリアを指定したい
といった場合にはアプリケーションに限定するだけでは不十分です。。
そこで、自分で指定したエリアで録画するアプリケーションを実装したいと考えました!
今回は、エリアを指定するためにRectangleを描画する方法をまとめたいと思います。
■xamlの実装
今回メインウィンドウとして実装したxamlはこちらになります。
ウィンドウにBorderで枠線を作成し、その中にCanvasを差し込んでいるシンプルな構成です。
Canvasの名前は"CanvasArea"としました。
Canvas上ではトリガとしてこちらを定義しました。
・マウスの左ボタン押す :MouseLeftDwn
・マウスの左ボタン上げる:MouseLeftUp
・マウスを動かす :MouseMoving
<Window x:Class="WpfApp2.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:WpfApp2" mc:Ignorable="d" Title="MainWindow" Height="408.739" Width="659.456"> <Grid> <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" Height="277" Margin="64,49,0,0" VerticalAlignment="Top" Width="528" Background="#00000000"> <Canvas x:Name="CanvasArea" HorizontalAlignment="Left" Height="273" Margin="0,1,0,0" VerticalAlignment="Top" Width="523" Panel.ZIndex="1" PreviewMouseLeftButtonUp="MouseLeftUp" PreviewMouseLeftButtonDown="MouseLeftDwn" PreviewMouseMove="MouseMoving" Background="White" /> </Border> </Grid> </Window>
■好きなエリアにRectangleを作成する
では早速、Canvas上にエリアを指定するためのRectangleを描画してみたいと思います。
コードはこちら。
using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace WpfApp2 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { bool isWriting = false; Point Init; private List<UIElement> RectangleList = new List<UIElement>(); UIElement RectElement = new UIElement(); public MainWindow() { InitializeComponent(); } private void MouseLeftDwn(object sender, MouseButtonEventArgs e) { Console.WriteLine("Click Mouse Dwn"); Canvas c = sender as Canvas; Init = e.GetPosition(c); c.CaptureMouse(); isWriting = true; } private void MouseLeftUp(object sender, MouseButtonEventArgs e) { if (isWriting) { Console.WriteLine("Click Mouse Up"); Canvas c = sender as Canvas; isWriting = false; c.ReleaseMouseCapture(); } } private void WriteRectangle(Point point) { CanvasArea.Children.Remove(RectElement); Rectangle rect = new Rectangle(); rect.Stroke = new SolidColorBrush(Colors.Red); rect.StrokeThickness = 1; rect.Width = Math.Abs(Init.X - point.X); rect.Height = Math.Abs(Init.Y - point.Y); if(Init.X < point.X) { Canvas.SetLeft(rect, Init.X); } else { Canvas.SetLeft(rect, point.X); } if(Init.Y < point.Y) { Canvas.SetTop(rect, Init.Y); } else { Canvas.SetTop(rect, point.Y); } CanvasArea.Children.Add(rect); RectElement = rect; } private void MouseMoving(object sender, MouseEventArgs e) { if (isWriting) { Console.WriteLine("Click Mouse Move"); Point pos = e.GetPosition(CanvasArea); WriteRectangle(pos); } } } }
マウスの左ボタン押すとMouseLeftDwnが実行されます。
MouseLeftDwnでは、
・マウスの強制キャプチャ
・マウスの位置を記憶
・マウスを押下したフラグをtrue
します。
private void MouseLeftDwn(object sender, MouseButtonEventArgs e) { Console.WriteLine("Click Mouse Dwn"); Canvas c = sender as Canvas; Init = e.GetPosition(c); c.CaptureMouse(); isWriting = true; }
マウスの左ボタン押下した状態でマウスを動かすと、
MouseMovingがコールされます。
MouseMovingではWriteRectangleがコールされ、こちらでRectangleを描画します。
private void MouseMoving(object sender, MouseEventArgs e) { if (isWriting) { Console.WriteLine("Click Mouse Move"); Point pos = e.GetPosition(CanvasArea); WriteRectangle(pos); } }
最後にマウスの左ボタンを離すと、
MouseLeftUpがコールされます。
MouseLeftUpでは、
・マウスの強制キャプチャ解除
・マウスを押下したフラグをfalse
private void MouseLeftUp(object sender, MouseButtonEventArgs e) { if (isWriting) { Console.WriteLine("Click Mouse Up"); Canvas c = sender as Canvas; isWriting = false; c.ReleaseMouseCapture(); } }
このコードを実行すると、
こちらのような結果となります。
■Rectangle描画領域をCanvasに限定する
先ほどのコードでRectangleを好きな位置に描画することが出来るのですが、
領域外にRectangleをはみ出して描画できてしまいます。
次に、Canvas領域内に限定してRectangleを留めるようなコードを実装してみます。
変更するのは、WriteRectangleのみになります。
コードはこちら。
private void WriteRectangle(Point point) { CanvasArea.Children.Remove(RectElement); Rectangle rect = new Rectangle(); rect.Stroke = new SolidColorBrush(Colors.Red); rect.StrokeThickness = 1; rect.Width = Math.Abs(Init.X - point.X); rect.Height = Math.Abs(Init.Y - point.Y); if (point.X > CanvasArea.ActualWidth) { Canvas.SetLeft(rect, Init.X); rect.Width = CanvasArea.ActualWidth - Init.X; } else if (point.X < 0) { Canvas.SetLeft(rect, 0); rect.Width = Init.X; } else if (Init.X < point.X) { Canvas.SetLeft(rect, Init.X); } else { Canvas.SetLeft(rect, point.X); } if (point.Y > CanvasArea.ActualHeight) { Canvas.SetTop(rect, Init.Y); rect.Height = CanvasArea.ActualHeight - Init.Y; } else if (point.Y < 0) { Canvas.SetTop(rect, 0); rect.Height = Init.Y; } else if (Init.Y < point.Y) { Canvas.SetTop(rect, Init.Y); } else { Canvas.SetTop(rect, point.Y); } CanvasArea.Children.Add(rect); RectElement = rect; }
実施していることですが、Canvasのエリア外に出てしまった場合にはサイズを
Canvasサイズ - 初期座標位置
とすることで、Canvasサイズ外にRectangleがはみ出さないように実装しております。
実際に動かすとこちらのようになります。
■最後に
今回はWPFで自由に位置やサイズを指定してRectangleを描画するコードをまとめました。
これをスクリーン録画とマージしてエリア指定して録画するアプリにしていきたいと思います!!
特定のアプリに限定したスクリーン録画を試みる
先日、OpenCVSharpを用いて画面全体でのスクリーン録画を試してみました。
elsammit-beginnerblg.hatenablog.com
ですがこの方法ですと、
特定のアプリが映っている領域のみ録画したい場合には無駄な領域も写ってしまいますね。
後で余計な領域を削除するのも大変なので、今回は特定のアプリケーションの領域のみを録画する方法をまとめたいと思います!!
■前提条件
アプリケーション領域に限定したスクリーン録画の方法をまとめるにあたり、
前回の画面全体のスクリーン録画で用いたコードを流用します。
このため本ブログではxamlコードの掲載は割愛します。
必要に応じて前回のブログをご参照ください。
■アプリケーションの座標やサイズ取得方法
アプリケーションの領域に限定したスクリーン録画をまとめる前に、
アプリケーション領域を特定するための座標やサイズを取得する方法をまとめます。
アプリケーションの座標やサイズ取得するにあたり、
Windows APIの1つであるuser32.dllを用います。
user32.dllを用いるにあたり、
using System.Runtime.InteropServices;
を定義ください。
さらに、user32.dllのGetWindowRect()APIを用いるためあらかじめ、
こちらのコードも定義しておきます。
[DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left; public int top; public int right; public int bottom; }
GetWindowRectは該当するウィンドウハンドルに対する座標情報を返すAPIになります。
引数ですが、
・IntPtr hwnd:ウィンドウハンドル
・RECT lpRect:ウィンドウハンドルに対する座標情報
になります。
ここまでがアプリケーションの座標や領域の情報を取得するための前準備。
では実際に情報を取得する関数を作成していきます。
コードはこちら。
private bool GetAppPosition(ref RECT rect) { bool flag = false; string appName = "notepad"; //ここに実行ファイル名(拡張子なし)を記入する. try { var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle; flag = GetWindowRect(mainWindowHandle, out rect); } catch { flag = false; } return flag; }
appName変数に記入したアプリケーション名(実行ファイル名)の座標を取得するコードになります。
さらに、アプリケーション名が存在しない場合や領域取得できなかった場合にエラーとするためにOK/NGを示すflagを返すようにしております。
System.Diagnostics.Process.GetProcessesByName(appName)は、
引数に与えたアプリケーション名を元に全てのプロセス情報を返すAPIになっております。
今回は取得できたプロセス情報のうち1つ目に見つけられたプロセス情報のみを抽出した上で、
プロセス情報のうち、ウィンドウハンドル情報をmainWindowHandle 変数に代入しています。
var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle;
後は先ほど用意した、
GetWindowRect(mainWindowHandle, out rect);
でウィンドウハンドルから座標情報を取得して終了です。
■特定アプリの領域を切り出してスクリーン録画してみる
では本題の特定アプリの領域を切り出してスクリーン録画を行う方法をまとめます。
といっても座標情報を先ほどのコードで得られるので後は領域指定して録画するのみになります。
全体のコードはこちら。
using System; using System.Windows; using System.Windows.Media.Imaging; using System.Drawing; using System.Windows.Interop; using System.Threading; using OpenCvSharp; using OpenCvSharp.Extensions; using System.Runtime.InteropServices; namespace WpfApp1 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : System.Windows.Window { [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left; public int top; public int right; public int bottom; } public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(new ThreadStart(() => { CaptureMovieAsync(); })); thread.Start(); } private void CaptureMovieAsync() { using (var writer = new VideoWriter("test.wmv", FourCC.WMV3, 5, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight))) { for (int i = 0; i < 100; i++) { RECT rect = new RECT(); bool flag = GetAppPosition(ref rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; using (var screenBmp = new System.Drawing.Bitmap( width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpGraphics = Graphics.FromImage(screenBmp)) { bmpGraphics.CopyFromScreen(rect.left, rect.top, 0, 0, screenBmp.Size); Dispatcher.Invoke((Action)(() => { ImgCap.Source = Imaging.CreateBitmapSourceFromHBitmap( screenBmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); Mat mat = BitmapConverter.ToMat(screenBmp).CvtColor(ColorConversionCodes.RGB2BGR); Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2RGB); Cv2.Resize(mat, mat, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)); writer.Write(mat); })); } } Thread.Sleep(100); } } } private bool GetAppPosition(ref RECT rect) { bool flag = false; string appName = "notepad"; //ここに実行ファイル名(拡張子なし)を記入する. try { var mainWindowHandle = System.Diagnostics.Process.GetProcessesByName(appName)[0].MainWindowHandle; flag = GetWindowRect(mainWindowHandle, out rect); } catch { flag = false; } return flag; } } }
追加・変更したコードはこちらのみ。
for (int i = 0; i < 100; i++) { RECT rect = new RECT(); bool flag = GetAppPosition(ref rect); int width = rect.right - rect.left; int height = rect.bottom - rect.top; using (var screenBmp = new System.Drawing.Bitmap( width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpGraphics = Graphics.FromImage(screenBmp)) { bmpGraphics.CopyFromScreen(rect.left, rect.top, 0, 0, screenBmp.Size);
実施していることは、
GetAppPosition()関数にて取得できたRECT情報からアプリケーションのサイズを算出。
算出した結果をscreenBmp定義時にサイズ情報として入力かつ、
CopyFromScreen()関数にて取得するスクリーンショットの領域をアプリケーションの領域に指定しているのみになります。
アプリケーションの座標さえ手に入ってしまえば後はスクリーンショットとして取得する領域を合わせるだけですね。
■最後に
今回は特定のアプリケーションに限定したスクリーン録画を試してみました。
最近スクリーンショット取得すること多いので利用していきたいと思います。
WPFでスクリーン録画してみる
先日、WPFでスクリーンショットやスクリーン画面の動画を表示してみました。
elsammit-beginnerblg.hatenablog.com
今回はこちらのコードに録画機能を追加してスクリーン上の映像を録画してみようと思います!!
■OpenCVSharpを準備する
録画するにあたり、OpenCVSharpを用います。
OpenCVのC#版ですね。
OpenCVSharpですが、
Visual Studioを用いている場合には
Nugetパッケージ管理
を用いれば簡単にインストールが可能です。
Nugetパッケージ管理ですが、
ソリューションエクスプローラー上の参照を右クリックすると
こちらの通り、選択項目が出てくるのでこちらをクリック。
Nugetパッケージ管理が表示されたら、
上タブから「参照」を選択し、検索画面で「OpenCVSharp」と入力すると
こちらの通り複数バージョンのOpenCVSharpがリストで出力されます。
今回は、
・OpenCvSharp4
・OpenCvSharp4.runtime.win
をインストールしました。
合わせて、Extensionsも利用したいので、
・OpenCvSharp4.Extensions
もインストールします。
これでOpenCVSharpのインストールは完了です。
■WPFでスクリーン録画してみる
では本題のスクリーン録画を行ってみます。
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> <Image x:Name="ImgCap" HorizontalAlignment="Left" Height="343" VerticalAlignment="Top" Width="525" Margin="62,34,0,0"/> <Button Content="start" HorizontalAlignment="Left" Height="54" Margin="656,351,0,0" VerticalAlignment="Top" Width="113" Click="Button_Click"/> </Grid> </Window>
録画開始するためのボタンと録画中の映像を表示するImagが1つずつあるのみの構成です。
次にC#のコードですが、こちらの通りになります。
using System; using System.Windows; using System.Windows.Media.Imaging; using System.Drawing; using System.Windows.Interop; using System.Threading; using OpenCvSharp; using OpenCvSharp.Extensions; namespace WpfApp1 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : System.Windows.Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(new ThreadStart(() => { CaptureMovieAsync(); })); thread.Start(); } private void CaptureMovieAsync() { using (var writer = new VideoWriter("test.wmv", FourCC.WMV3, 5, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight))) { for (int i = 0; i < 100; i++) { using (var screenBmp = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpGraphics = Graphics.FromImage(screenBmp)) { bmpGraphics.CopyFromScreen(0, 0, 0, 0, screenBmp.Size); Dispatcher.Invoke((Action)(() => { ImgCap.Source = Imaging.CreateBitmapSourceFromHBitmap( screenBmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); Mat mat = BitmapConverter.ToMat(screenBmp).CvtColor(ColorConversionCodes.RGB2BGR); Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2RGB); Cv2.Resize(mat, mat, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)); writer.Write(mat); })); } } Thread.Sleep(100); } } } } }
前回のスクリーンショット映像表示のみのコードから追加した点は2点です。
1点目は下記コードになります。
using (var writer = new VideoWriter("test.wmv", FourCC.WMV3, 5, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)))
VideoWriterで動画ファイルへの書き込み用オブジェクトを生成します。
動画ファイルはwmvで出力したかったので、FourCCはWMV3としました。
また、動画サイズは画面サイズと合致するため、
new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)
としました。
2点目は下記のコードになります。
Mat mat = BitmapConverter.ToMat(screenBmp).CvtColor(ColorConversionCodes.RGB2BGR); Cv2.CvtColor(mat, mat, ColorConversionCodes.BGR2RGB); Cv2.Resize(mat, mat, new OpenCvSharp.Size((int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight)); writer.Write(mat);
こちらは、BitmapをMat型に型変換を行った後、
BGR⇒RGBに色変換とサイズ変更後、1点目で記載したVideoWriterへWriteしています。
本ループですが、、、
100ミリ秒のwaitで0から100までループさせているので、計10秒間実施しています。
要するに10秒間のスクリーン画面の録画を行っている、ということですね。
■最後に
今回はスクリーン画面を録画する方法をまとめてみました。
OpenCVが利用できるので、録画以外にもいろいろなことが出来そうですね。
WPFでスクリーンショットや画面操作動画表示
今回はC#でスクリーンショットや画面をリアルタイムで表示する方法をまとめたいと思います。
最終的にはC#で画面録画を実現するまでをまとめたいと思いますが、
今回は画面をWPFのImageに表示するまでをまとめていきます。
■前提条件
今回画面レイアウトですが、こちらの通り
ImageタグとButtonタグのみの構成とします。
用意したボタンをトリガにスクリーンショットや操作映像をImageに表示させてみます。
ボタン押下時に実行される関数名は、「Button_Click」としております。
<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> <Image x:Name="ImgCap" HorizontalAlignment="Left" Height="343" VerticalAlignment="Top" Width="525" Margin="62,34,0,0"/> <Button Content="Button" HorizontalAlignment="Left" Height="54" Margin="656,351,0,0" VerticalAlignment="Top" Width="113" Click="Button_Click"/> </Grid> </Window>
■スクリーンショット実現方法
まずはWPFで画面のスクリーンショットです。
ボタンを押下した時にスクリーンショットを画面に表示するコードをまとめます。
コードはこちら。
using System; using System.Windows; using System.Windows.Media.Imaging; using System.Drawing; using System.Windows.Interop; namespace WpfApp1 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : System.Windows.Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { ImgCap.Source = CopyScreen(); } private static BitmapSource CopyScreen() { using (var bmtpScreen = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpFromImg = Graphics.FromImage(bmtpScreen)) { bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size); return Imaging.CreateBitmapSourceFromHBitmap( bmtpScreen.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } } } } }
スクリーンショットを取得している関数ですが、
private static BitmapSource CopyScreen() { using (var bmtpScreen = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpFromImg = Graphics.FromImage(bmtpScreen)) { bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size); return Imaging.CreateBitmapSourceFromHBitmap( bmtpScreen.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } } }
になります。
まず、
bmtpScreen = new System.Drawing.Bitmap
にて新規でDrawing.Bitmap型の変数を生成します。
その後、
var bmpFromImg = Graphics.FromImage(bmtpScreen)
でGraphicsに変換後、
bmpFromImg.CopyFromScreen(0, 0, 0, 0, bmtpScreen.Size);
でWindows画面のスクリーンショット結果を取得します。
CopyFromScreen関数ですが、
引数はそれぞれ、
第1引数:転送元の四角形の左上隅の点の x 座標
第2引数:転送元の四角形の左上隅の点の y 座標
第3引数:転送先の四角形の左上隅の点の x 座標
第4引数:転送先の四角形の左上隅の点の y 座標
第5引数:転送される領域のサイズ
になります。
※参考:https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.graphics.copyfromscreen?view=dotnet-plat-ext-6.0
■動画化してみる
先ほどはボタン押下した際にその瞬間のスクリーンショットを表示するのみでした。
つまり静止画が表示しております。
今度は、ボタン押下後の画面を動画としてリアルタイム表示させる方法をまとめたいと思います。
コードはこちら。
using System; using System.Windows; using System.Windows.Media.Imaging; using System.Drawing; using System.Windows.Interop; using System.Threading; namespace WpfApp1 { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : System.Windows.Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(new ThreadStart(() => { CaptureMovieAsync(); })); thread.Start(); } private void CaptureMovieAsync() { for (int i = 0; i < 1000; i++) { using (var screenBmp = new System.Drawing.Bitmap( (int)SystemParameters.PrimaryScreenWidth, (int)SystemParameters.PrimaryScreenHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) { using (var bmpGraphics = Graphics.FromImage(screenBmp)) { bmpGraphics.CopyFromScreen(0, 0, 0, 0, screenBmp.Size); Dispatcher.Invoke((Action)(() => { ImgCap.Source = Imaging.CreateBitmapSourceFromHBitmap( screenBmp.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); })); } } Thread.Sleep(100); } } } }
今回のコードですが、100秒間画面をリアルタイムで表示させるコードになります。
やっていることは、
ボタン押下時にスレッド生成の上、動作実行し、
スレッド内で先ほどのスクリーンショットを連続で実行している
のみ。です。
スレッド側でオブジェクトの更新を行う必要があるので、
Dispatcher.Invokeを用いております。
■(補足)Dispatcher.Invokeとは?
WPFでは、メインスレッドのみオブジェクトにアクセスでき、別スレッドからオブジェクトにアクセスすることが出来ません。
理由ですが、
メインスレッドで所有するオブジェクトを別スレッドからアクセスできない
ためです。
そこで、別スレッドからアクセスするために
Dispatcher.Invoke
を用います。
このDispatcherはキューを管理するクラスになっており、
メインスレッドに対して、操作したいオブジェクトや処理をキューに格納し、
受け取ったメインスレッドは順次処理を実行しているイメージになります。
■参考
https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.graphics.copyfromscreen?view=dotnet-plat-ext-6.0
https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.graphics.fromimage?view=dotnet-plat-ext-6.0
https://araramistudio.jimdo.com/2017/05/02/c-
%E3%81%A7%E5%88%A5%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%8B%E3%82%89%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B/
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcher.invoke?view=windowsdesktop-6.0
OpenCVで映像のズームイン・ズームアウトを試してみる
今回はOpenCVで映像のズームイン・ズームアウトを行ってみたいと思います。
言語はPythonです。
■映像のズームイン・ズームアウトのコード
早速、ズームイン・ズームアウトのコードをご紹介します。
コードはこちら。
こちらは、2倍の倍率でズームインしているコードになります。
import cv2 scale = 2 #ここを変更すると倍率が変化する capture = cv2.VideoCapture(0) while(True): ret, frame = capture.read() height, width, channels = frame.shape[:3] if int(scale) < 1: scale = 1 roi_width = width / int(scale) roi_height = height / int(scale) sabun_w1 = int((width - roi_width)/2) sabun_w2 = int((width + roi_width)/2) sabun_h1 = int((height - roi_height)/2) sabun_h2 = int((height + roi_height)/2) frame = frame[sabun_w1:sabun_w2, sabun_h1:sabun_h2] frame = cv2.resize(frame, (width, height)) cv2.imshow('frame',frame) if cv2.waitKey(1) & 0xFF == ord('q'): break capture.release() cv2.destroyAllWindows()
ズームインの流れをまとめていきます。
一例としてこちらの画像を例に用います。
まずは中央の枠の領域を指定するための座標を算出します。
算出しているコードはこちらにあたります。
roi_width = width / int(scale) roi_height = height / int(scale) sabun_w1 = int((width - roi_width)/2) sabun_w2 = int((width + roi_width)/2) sabun_h1 = int((height - roi_height)/2) sabun_h2 = int((height + roi_height)/2)
こちらのコードでしている範囲を画像で表すとこちらの範囲になります。
次に枠内を切り取ります。
切り取りのコードはこちらにあたります。
frame = frame[sabun_w1:sabun_w2, sabun_h1:sabun_h2]
切り取り後の画像はこちらになります。
最後に、切り取り後の画像に対して元の画像サイズにリサイズすることによりズームインが実現出来ます。
該当コードはこちら。
frame = cv2.resize(frame, (width, height))
最終的な画像はこちらの通りとなり、中央領域の画像が拡大します。
■動的に拡大率を変更するには
ここまでで、倍率を変更してズームイン・ズームアウトを行う方法をまとめてきました。
次にコンソール上で倍率を入力すると動的にズームイン・ズームアウトを行うコードをまとめていきます。
import cv2 import threading scale = 1 def input_keybord(): while True: global scale scale = input("Enter scale: ") capture = cv2.VideoCapture(0) thread1 = threading.Thread(target=input_keybord) thread1.start() while(True): ret, frame = capture.read() height, width, channels = frame.shape[:3] if int(scale) < 1: scale = 1 roi_width = width / int(scale) roi_height = height / int(scale) sabun_w1 = int((width - roi_width)/2) sabun_w2 = int((width + roi_width)/2) sabun_h1 = int((height - roi_height)/2) sabun_h2 = int((height + roi_height)/2) frame = frame[sabun_w1:sabun_w2, sabun_h1:sabun_h2] frame = cv2.resize(frame, (width, height)) cv2.imshow('frame',frame) if cv2.waitKey(1) & 0xFF == ord('q'): break capture.release() cv2.destroyAllWindows()
先ほど解説したズームインコードとの差分は、
def input_keybord(): while True: global scale scale = input("Enter scale: ")
をスレッドで実行してscaleをコンソール上で変更できるようにした点です。
コンソール上で整数値を入力すれば、それに合わせてscaleが変わるので、
倍率が変化してズームイン・ズームアウトが行える原理です。
■実際に動かしてみる
実際にコードを動かして、ズームイン/ズームアウトするか確認していきます。
結果はこちら!!
問題なくズームイン・ズームアウトが実現出来ました。
youtu.be
React Router v6でページ遷移方法をまとめる
昔、React Routerでページ遷移に関してまとめました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com
どうやらReact Routerがv5⇒v6に上がったことでページ遷移の方法が変わったようです。
今回はv6でのページ遷移についてまとめていきたいと思います。
■条件
今回はページとして、
・First
・Second
・Third
を用意し、
FirstからボタンクリックでSecondとThirdそれぞれにページ遷移させる方法をまとめます。
■React Router v6でのページ遷移方法
では早速、React Router v6でのページ遷移方法をまとめていきます。
まずはルーティングの設定です。
v6ではこちらのようにコードを記載します。
import { BrowserRouter, Route,Routes } from 'react-router-dom' import Third from './third' import Second from './second' import First from './first' function App() { return ( <div className="App"> <BrowserRouter> <div> <Routes> <Route path='/third' element={<Third />}/> <Route path='/second' element={<Second />}/> <Route path='/' element={<First />}/> </Routes> </div> </BrowserRouter> </div> ); } export default App;
v6では、
<Routes> <Route path='/third' element={<Third />}/> <Route path='/second' element={<Second />}/> <Route path='/' element={<First />}/> </Routes>
というようにRoutesを用いる必要があります。
次にfirstからsecondへ遷移させるためのコードをまとめます。
firstのページはこちらのようなコードになります。
import React, { Component } from 'react' import { Link } from 'react-router-dom'; export default class first extends Component { render () { return( <div> <p>View first page!!</p> <button><Link to={"/second"}>Go to Second Page</Link></button> </div> ) } }
そして、secondページはこちらのように実装しました。
import React, { Component } from 'react' export default class second extends Component { render () { return( <div> <p>View Second page</p> </div> ) } }
ここで重要になるのは、
ページ遷移では、Linkタグを用いるということです。
toで指定したルートへページ遷移させることが出来ます。
今回はsecondに遷移させたいので、
<Link to={"/second"}>Go to Second Page</Link>
としております。
■パラメータを渡しながらページ遷移
では次にパラメータを渡しながらページ遷移をしてみます。
今後は、firstからthirdへパラメータ「hogehoge」を渡してみたいと思います。
firstのコードですが、先ほどのコードの差分のみを掲載します。
render () { return( <div> <p>View first page!!</p> <button><Link to={"/second"}>Go to Second Page</Link></button> <button><Link to={"/third"} state={{test:"hogehoge"}}>Go to Third Page</Link></button> </div> ) }
追加したコードですが、
<button><Link to={"/third"} state={{test:"hogehoge"}}>Go to Third Page</Link></button>
となります。
パラメータを渡す場合には、
state={{test:"hogehoge"}}
といったjson形式でstateへ定義していけばよいです。
次にthridページを実装していきます。
コードはこちら。
import React, { Component } from 'react' import { useLocation } from 'react-router-dom'; class Third extends Component{ render(){ return <p>View Third page</p> } } export default () => { const location = useLocation(); const { st} = location.state; return (<div> <Third></Third> {st} </div> ); };
パラメータを受け取る際、useLocationを用います。
ただし、useLocationはclassでは用いることが出来ないようなので、
export default () => { const location = useLocation(); const { test} = location.state; ・・・
というように、useLocationを用いてかつ location.stateから
firstでパラメータとして渡されたtest:"hogehoge"を受け取ります。
これにより、変数testに"hogehoge"が代入されました。
合わせて今回は、
class Third extends Component{ render(){ return <p>View Third page</p> } }
でclassを別途定義してみました。
今回は簡単にView Third pageを表示するのみになります。
■実際に動かしてみる。
では、こちらのコードを実際に動かしてみたいと思います!!
まずは、first⇒secondへのページ遷移です。
firstからsecondへページ遷移出来ているのが確認できました。
次にfirst⇒thirdへのページ遷移です。
firstからthirdへページ遷移出来ておりかつ、変数として与えたhogehogeが表示されているのが確認できました。
■最後に
今回はReact Router v6でのページ遷移方法に関してまとめてみました。
まだまだ不慣れな点があるのでもう少し勉強してみます!!
学んだことはブログにまとめていきたいな。