C#のフォームにUSBカメラからOpenCvSharpで動画を取り込む

　Windowsのカメラアプリが、PC環境の問題なのか長時間起動していると画面が灰色に表示され、OSを再起動しないとプレビューを表示できなくなる現象があった。USBカメラに対応したフリーソフトを探してもいいが、折角の機会なのでC#/OpenCvSharpで最低限必要な機能を作ってみた(プレビューを表示してボタンでキャプチャするだけの本当に最低限の機能だけ)。

　OpenCvSharpはNuGetから4.10.0.20240616をインストールしている。

## GUI

　PictureBox1およびButton1を配置する。
　PictureBox1のAnchorはTop, Bottom, Left, Rightとし、Button1と合わせていい感じの場所に配置しておく。

## コード

　フォームのコンストラクタ内に以下のコードを貼り付ける。

```C#
Progress<object?> pictureBoxRefresh =
    new Progress<object?>(_ => pictureBox1.Refresh());

_ = Task.Run(() =>
{
    using OpenCvSharp.VideoCapture cap = new(0)
    {
        FrameWidth = 1920,
        FrameHeight = 1080,
    };
    using OpenCvSharp.Mat frameBuff = new();
    using OpenCvSharp.Mat resizeDst1 = new();
    using OpenCvSharp.Mat resizeDst2 = new();

    var resizeBuff = resizeDst1;
    OpenCvSharp.Size dstSize = default;

    var isSrcCapture = false;
    button1.Click += (_, _)
     => isSrcCapture = true;

    var isDstSizeChange = true;
    pictureBox1.SizeChanged += (_, _)
     => isDstSizeChange = true;

    while (true)
    {
        if (!cap.Read(frameBuff))
        { continue; }

        if (isSrcCapture)
        {
            isSrcCapture = false;
            saveImage(frameBuff);
        }

        if (isDstSizeChange)
        {
            isDstSizeChange = false;
            dstSize = calcNewSize(frameBuff.Size(), pictureBox1.Size);

            if (dstSize.Width == 0)
            { continue; }

            resizeBuff = resizeBuff != resizeDst1 ? resizeDst1 : resizeDst2;

            OpenCvSharp.Cv2.Resize(frameBuff, resizeBuff, dstSize);

            using var dispose = pictureBox1.Image;
            pictureBox1.Image = new Bitmap(
                resizeBuff.Width, resizeBuff.Height, (int)resizeBuff.Step(),
                System.Drawing.Imaging.PixelFormat.Format24bppRgb, resizeBuff.Data);
        }

        if (dstSize.Width == 0)
        { continue; }

        OpenCvSharp.Cv2.Resize(frameBuff, resizeBuff, dstSize);

        pictureBoxRefresh.Report(null);
    }

    ; static OpenCvSharp.Size calcNewSize(OpenCvSharp.Size srcSize, Size dstSize)
    {
        var scale = double.Min(
            (double)dstSize.Width / srcSize.Width,
            (double)dstSize.Height / srcSize.Height);
        var w = (int)(scale * srcSize.Width) & ~3;
        var h = (int)((double)w / srcSize.Width * srcSize.Height);

        return new(w, h);
    }

    ; static void saveImage(OpenCvSharp.Mat frame)
    {
        var path = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
            @$"Camera Roll\{DateTime.Now:yyyyMMddTHHmmss}.jpg");

        var dir = Path.GetDirectoryName(path) ?? throw new Exception();
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }

        using Bitmap bmp =
            OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame);

        bmp.Save(path);
    }
});
```

## 雑記

　PictureBoxの大きさをFormの大きさに追従させているので、サイズ変化時のGUIの再描画周りでメモリアクセスの競合を避けるためにややこしいコードになっている(自分でPictureBox.Sizeを書き換えたほうが簡単に処理できるかもしれない)。

　Bitmapを作成する際はOpenCVのMatの生データへのポインタを渡している。Bitmap側の制限で横方向の生データが4バイトの整数倍である必要がある(OpenCVのMatでパディングができない)ため、リサイズの解像度を4の整数倍に切り捨てている。