Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

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を用います。
OpenCVC#版ですね。

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はキューを管理するクラスになっており、
メインスレッドに対して、操作したいオブジェクトや処理をキューに格納し、
受け取ったメインスレッドは順次処理を実行しているイメージになります。

■最後に

今回はWPFスクリーンショットや画面をリアルタイム動画で表示する方法でまとめました。
次は表示したスクリーンショットや動画を保存・録画する方法についてまとめたいと思います。



OpenCVで映像のズームイン・ズームアウトを試してみる

今回はOpenCVで映像のズームイン・ズームアウトを行ってみたいと思います。
言語はPythonです。



Pythonバージョン

使用するPythonのバージョンですが、
Python 3.7.4
になります。

■映像のズームイン・ズームアウトのコード

早速、ズームイン・ズームアウトのコードをご紹介します。
コードはこちら。
こちらは、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()

ズームインの流れをまとめていきます。
一例としてこちらの画像を例に用います。
f:id:Elsammit:20220414222340j:plain

まずは中央の枠の領域を指定するための座標を算出します。
算出しているコードはこちらにあたります。

    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)

こちらのコードでしている範囲を画像で表すとこちらの範囲になります。
f:id:Elsammit:20220414222714p:plain

次に枠内を切り取ります。
切り取りのコードはこちらにあたります。

frame = frame[sabun_w1:sabun_w2, sabun_h1:sabun_h2]

切り取り後の画像はこちらになります。
f:id:Elsammit:20220414223117p:plain

最後に、切り取り後の画像に対して元の画像サイズにリサイズすることによりズームインが実現出来ます。
該当コードはこちら。

frame = cv2.resize(frame, (width, height))

最終的な画像はこちらの通りとなり、中央領域の画像が拡大します。
f:id:Elsammit:20220414223510p:plain

■動的に拡大率を変更するには
ここまでで、倍率を変更してズームイン・ズームアウトを行う方法をまとめてきました。
次にコンソール上で倍率を入力すると動的にズームイン・ズームアウトを行うコードをまとめていきます。

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

■最後に

今回はOpenCVでズームイン・ズームアウトする方法をまとめてみました。
OpenCVは様々なことが実現出来、触っていて楽しいです。



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へページ遷移出来ているのが確認できました。
f:id:Elsammit:20220410225136g:plain

次にfirst⇒thirdへのページ遷移です。
firstからthirdへページ遷移出来ておりかつ、変数として与えたhogehogeが表示されているのが確認できました。
f:id:Elsammit:20220410225159g:plain

■最後に

今回はReact Router v6でのページ遷移方法に関してまとめてみました。
まだまだ不慣れな点があるのでもう少し勉強してみます!!
学んだことはブログにまとめていきたいな。



flaskでREST APIを体験してみる

今更感ありますが、、、今回はREST APIについてまとめていきたいと思います。
また、REST APIについて記事や本を読んでもよく分からず、
実際にREST APIを体験するためにflaskでREST APIを作成してみましたので、
備忘録も兼ねてブログにまとめたいと思います!!



REST APIとは?

そもそもRESTとは、、
REpresentational State Transferの略になります。

このRESTに関して、RESTの4原則があり、この4原則を満たすものをRESTfulと呼んだりします。
RESTの4原則ですが、

1.セッションなどの状態管理を行わない。(やり取りされる情報はそれ自体で完結して解釈することができる)
2.情報を操作する命令の体系が予め定義・共有されている。(HTTPのGETやPOSTメソッドなど)
3.すべての情報は汎用的な構文で一意に識別される。(URLやURIなど)
4.情報の内部に、別の情報や(その情報の別の)状態へのリンクを含めることができる。

になります。

このRESTの考えを用いてAPIを設計・作成したものをREST APIと言います。

具体的には、
RESTful APIとは、Webシステムを外部から利用するためのプログラムの呼び出し規約(API)の種類の一つで、
「REST」(レスト)と呼ばれる設計原則に従って策定されたもの。
RESTそのものは適用範囲の広い抽象的なモデルだが、
一般的にはRESTの考え方をWeb APIに適用したものをRESTful APIと呼んでいる。
※参考:RESTful API(REST API)とは - IT用語辞典 e-Words

とのことです!!

具体的にはこちらの記事をご参照ください。
0からREST APIについて調べてみた - Qiita

こちらに掲載されている、
RESTなAPIとそうではないAPIの例
が個人的に分かりやすく、REST APIに関して理解が進みました。

■FlaskでREST APIを作成してみる

REST APIに関して文字でまとめてみましたが、、
文字だけではよく分からない!!と思いましたので、実際に作成して動かすことで体験してみました。

今回使用するのはpythonのFlaskです。
Flaskを使用した理由ですが、
個人的に一番使い慣れたサーバーサイドのWebアプリケーションフレームワークだったためです。

では早速コードを載せます。
今回作成したコードはこちら!!

from flask import Flask, jsonify, abort, request
import json

app = Flask(__name__)

datas = [
    {"id":1, "name":"hoge", "category":"manga","quantity":5},
    {"id":2, "name":"huga", "category":"magazine","quantity":10},
    {"id":3, "name":"hogehuga", "category":"magazine","quantity":1}
]

# 一覧取得
@app.route('/bookstore', methods=['GET'])
def findBookFromName():
    retList = []
    
    for d in datas:
        retList.append(d)

    if len(retList) > 0:
        return jsonify(retList)
    else:
        return "",400

# idで指定した項目の抽出
@app.route('/bookstore/<int:id>', methods=['GET'])
def findBookFromId(id):
    ret = ""
    for d in datas:
        if d["id"] == id:
            ret = d
    
    if ret != "":
        return jsonify(ret)
    else:
        return "",400

# データ追加
@app.route('/bookstore', methods=['POST'])
def addBooks():
    postedData = request.data.decode('utf-8')
    postedData = json.loads(postedData)
    bookData = {
        "id":int(postedData["id"]),
        "name":postedData["name"],
        "category":postedData["category"],
        "quantity":int(postedData["quantity"])
    }
    datas.append(bookData)

    return '',200

#データ更新
@app.route('/bookstore/', methods=['PUT'])
def putBook():
    putData = request.data.decode('utf-8')
    putData = json.loads(putData)
    ret = ""
    print(putData["name"])
    for d in datas:
        if d["id"] == putData["id"]:
            ret = d

    ret["name"] = putData["name"]
    ret["category"] = putData["category"]
    ret["quantity"] = putData["quantity"]

    return '',200


if __name__ == "__main__":
    app.run(debug=True)

APIはそれぞれこちらのようになります。
・一覧取得        :('/bookstore', methods=['GET'])
・idで検索したデータを抽出 :app.route('/bookstore/', methods=['GET'])
・データ追加       :app.route('/bookstore', methods=['POST'])
・データ更新       :app.route('/bookstore/', methods=['PUT'])

では動作確認をしていきます。
curlコマンドを用いてレスポンスを確認する最も簡単になります。

先ほどのコードを実行した上でcurlコマンドを用いて、

curl GET http://127.0.0.1:5000/bookstore

と実行してみてください。

すると、

[
  {
    "category": "manga",
    "id": 1,
    "name": "hoge",
    "quantity": 5
  },
  {
    "category": "magazine",
    "id": 2,
    "name": "huga",
    "quantity": 10
  },
  {
    "category": "magazine",
    "id": 3,
    "name": "hogehuga",
    "quantity": 1
  }
]

といったjson形式であらかじめ登録していたdata変数が表示されるかと思います。

datas = [
    {"id":1, "name":"hoge", "category":"manga","quantity":5},
    {"id":2, "name":"huga", "category":"magazine","quantity":10},
    {"id":3, "name":"hogehuga", "category":"magazine","quantity":1}
]

■最後に

今回はREST APIに関してまとめてみました。
基本的な部分は理解できたのですが、実際にREST APIで複雑なWebアプリを作成する場合にはどうなるんだろう??
逆に複雑化したりしないのかな?
今度Webアプリ作成する際には意識してみようかな?