Newer
Older
TIASShot / TIASshot / SelfTest / SelfTestCamera.cs
#if SELFTEST
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace TIASshot {

    /// <summary>
    /// セルフテスト用の偽カメラ(SELFTEST 限定).
    /// 実カメラ SDK(Lumenera / IC Imaging Control)には一切触れず,合成フレームを
    /// 専用スレッド(SDK のコールバックスレッドの代替)から流す.
    /// CameraBase の実コード(BootCheck・ProcessCalibrationFrame・DetectChart・CalcTcc・
    /// ShotOne → RunShotLoop → SaveThread → SaveImages)をそのまま通すことを目的とし,
    /// 本クラスで代替するのはカメラ固有部分(フレーム生成・ゲイン反映・撮影開始/終了)のみ.
    /// 結合ロジックテスト(層②)で失敗を注入できるよう sealed にしない(テスト側の派生で
    /// CaptureFrame・BeginShot 等を上書きして例外を投げさせる等.本番コードの挙動には影響しない).
    /// </summary>
    internal class SelfTestCamera : CameraBase {

        // 合成フレームの生成元(呼び出しごとに新しい Mat を返す.所有権は本クラスへ移る)
        private readonly Func<int, Mat> _frameFactory;

        // 疑似ゲイン(ApplyWhiteBalance で更新し,生成フレームに乗算する)
        private readonly object _gainLock = new object();
        private double _gainB = 1.0;
        private double _gainG = 1.0;
        private double _gainR = 1.0;

        private Thread _previewThread;
        // 切断時にプレビュースレッドの終了を待つ上限
        private static readonly TimeSpan DisconnectJoinTimeout = TimeSpan.FromSeconds(3);
        private volatile bool _stopPreview;
        private int _frameIndex;
        private int _framesDelivered;

        // 本クラスが生成した表示用入力 Bitmap(同一性の比較専用.解放は PreviewCallback の二重バッファが行う)
        private readonly List<Bitmap> _producedBitmaps = new List<Bitmap>();
        private readonly object _producedLock = new object();

        public SelfTestCamera(Form1 form, Func<int, Mat> frameFactory) : base(form) {
            _frameFactory = frameFactory;
        }

        /// <summary>プレビュースレッドで発生した例外(無ければ null)</summary>
        public Exception PreviewError { get; private set; }

        /// <summary>コールバックへ渡したフレーム数</summary>
        public int FramesDelivered => Volatile.Read(ref _framesDelivered);

        /// <summary>自動校正が完了したか(CameraBase.CalcTcc が立てるフラグ)</summary>
        public bool IsCalibrated => _calibrated;

        /// <summary>CalcTcc が校正データを保存したフォルダ(RecordCalibrationInfo 時点で記録)</summary>
        public string CalibrationFolder { get; private set; }

        /// <summary>直近の撮影データの保存フォルダ</summary>
        public string CurrentSaveFolder => _saveFolder;

        /// <summary>プレビュースレッドが動作中か</summary>
        public bool IsPreviewRunning => _previewThread != null && _previewThread.IsAlive;

        /// <summary>
        /// 現在のチャートマスク(_chartMasks)の参照のコピー(古い順.検出前・切断後は空).
        /// 再検出・切断でマスクが解放された(Mat.IsDisposed)ことを確かめるために使う.
        /// _chartMasks はプレビュースレッドが書き換えるため,プレビュー停止中(IsPreviewRunning が false)に読むこと.
        /// 返した Mat の所有権は本クラスに残る(呼び出し側で Dispose しない).
        /// </summary>
        public IReadOnlyList<Mat> ChartMasksSnapshot => _chartMasks.ToArray();

        public override bool Connect() {
            if (!BootCheck()) return false;
            DeviceName = "SelfTestCamera";
            SerialNumber = "SELFTEST";
            SetInfo("カメラ型番", DeviceName);
            SetInfo("カメラSN", SerialNumber);
            return true;
        }

        /// <summary>
        /// 切断.プレビュースレッドに停止を要求して終了を待ち(実カメラの SDK がストリーム停止で
        /// コールバックの終了を待つのと同じ),二重バッファの Bitmap とチャートマスクを解放する.
        /// プレビュースレッドから UI スレッドへの呼び出し(ShowMessage / EnableShots)は待たない投稿のため,
        /// UI スレッドから呼んでも相互待機にならない.アプリを閉じる処理(Form1_FormClosing)からも呼ばれる.
        /// 待機には上限を設け,超えた場合はログを残して解放へ進む.
        /// </summary>
        public override void Disconnect() {
            _stopPreview = true;
            var thread = _previewThread;
            if (thread != null && thread != Thread.CurrentThread && thread.IsAlive) {
                if (!thread.Join(DisconnectJoinTimeout)) {
                    Debug.WriteLine("警告: SelfTestCamera の切断でプレビュースレッドの終了待ちがタイムアウトしました");
                }
            }
            for (var i = 0; i < _bmps.Length; i++) {
                _bmps[i]?.Dispose();
                _bmps[i] = null;
            }
            ReleaseChartMasks();
            _isPreview = false;
        }

        /// <summary>
        /// プレビュー開始.SDK のコールバックスレッドの代わりに専用スレッドから合成フレームを流す.
        /// </summary>
        /// <param name="maxFrames">流すフレーム数の上限</param>
        /// <param name="intervalMs">フレーム間隔(ms)</param>
        /// <param name="stopWhenCalibrated">校正完了で停止するか</param>
        public void StartPreview(int maxFrames, int intervalMs, bool stopWhenCalibrated) {
            _stopPreview = false;
            _isPreview = true;
            _previewThread = new Thread(() => PreviewLoop(maxFrames, intervalMs, stopWhenCalibrated)) {
                IsBackground = true,
                Name = "SelfTestCamera.Preview",
            };
            _previewThread.Start();
        }

        /// <summary>プレビュー停止を要求する(待機は呼び出し元が UI メッセージを回しながら行う)</summary>
        public void RequestStopPreview() {
            _stopPreview = true;
        }

        private void PreviewLoop(int maxFrames, int intervalMs, bool stopWhenCalibrated) {
            try {
                for (var i = 0; i < maxFrames && !_stopPreview; i++) {
                    using (var imgt = CreateFrame()) {
                        PreviewCallback(imgt);
                    }
                    Interlocked.Increment(ref _framesDelivered);
                    if (stopWhenCalibrated && _calibrated) break;
                    if (intervalMs > 0) Thread.Sleep(intervalMs);
                }
            } catch (Exception ex) {
                // バックグラウンドスレッドの未処理例外はプロセスを落とすため,記録して判定側へ渡す
                Debug.WriteLine($"SelfTestCamera プレビュー例外: {ex}");
                PreviewError = ex;
            }
        }

        /// <summary>
        /// プレビューコールバック.Lucam.PreviewCallback の imgt 生成以降と同じ手順
        /// (二重バッファへ ToBitmap → 校正フレーム処理 → Form1.ShowImage → 反対側バッファを Dispose).
        /// ShowImage に渡した Bitmap の所有権は本クラスに残り,Form1 は読むだけ(FastClone でコピーする).
        /// </summary>
        private void PreviewCallback(Mat imgt) {
            var bmp = imgt.ToBitmap();
            _bmps[_bmpIndex] = bmp;
            lock (_producedLock) {
                _producedBitmaps.Add(bmp);
            }
            ProcessCalibrationFrame(imgt);
            _form.ShowImage(_bmps[_bmpIndex]);
            _bmpIndex = (_bmpIndex + 1) % 2;
            if (_bmps[_bmpIndex] != null) _bmps[_bmpIndex].Dispose();
        }

        /// <summary>指定の Image が本クラスの生成した入力 Bitmap そのもの(同一インスタンス)か</summary>
        public bool IsProducedBitmap(Image img) {
            if (img == null) return false;
            lock (_producedLock) {
                foreach (var b in _producedBitmaps) {
                    if (ReferenceEquals(b, img)) return true;
                }
            }
            return false;
        }

        /// <summary>現在の疑似ゲインを乗算した合成フレームを生成する(戻り値の所有権は呼び出し側)</summary>
        public Mat CreateFrame() {
            double gb, gg, gr;
            lock (_gainLock) {
                gb = _gainB;
                gg = _gainG;
                gr = _gainR;
            }
            var idx = Interlocked.Increment(ref _frameIndex) - 1;
            using (var baseFrame = _frameFactory(idx)) {
                var frame = new Mat();
                try {
                    Cv2.Multiply(baseFrame, new Scalar(gb, gg, gr), frame);
                    return frame;
                } catch {
                    frame.Dispose();
                    throw;
                }
            }
        }

        /// <summary>現在のチャートマスクで白色票(13 番)の平均値を計測する(校正完了後に使う)</summary>
        public Scalar MeasureWhitePatch() {
            using (var frame = CreateFrame()) {
                return Cv2.Mean(frame, _chartMasks[12]);
            }
        }

        /// <summary>現在の疑似ゲイン(B, G, R)</summary>
        public string DescribeGain() {
            lock (_gainLock) {
                return $"B={_gainB:0.000} G={_gainG:0.000} R={_gainR:0.000}";
            }
        }

        // ---- CameraBase のテンプレートメソッド(カメラ固有部分の代替) ----------------------

        protected override void ApplyWhiteBalance(Scalar whitePatch) {
            // Lucam と同じくゲインに更新比率(CameraBase.GetRatio=実コード)を掛ける
            lock (_gainLock) {
                _gainB *= GetRatio((float)whitePatch.Val0, Config.GetFloat("Calib/Reference/B"));
                _gainG *= GetRatio((float)whitePatch.Val1, Config.GetFloat("Calib/Reference/G"));
                _gainR *= GetRatio((float)whitePatch.Val2, Config.GetFloat("Calib/Reference/R"));
            }
        }

        protected override void LogCalibrationConverged() {
            Debug.WriteLine($"SelfTestCamera ゲイン収束: {DescribeGain()}");
        }

        protected override void RecordCalibrationInfo(Mat imgt) {
            CalibrationFolder = _saveFolder;
            SetInfo("SelfTestGain", DescribeGain());
        }

        /// <summary>
        /// 撮影セッション本体.CameraBase.Shot の XML ドキュメントに記載のライフサイクル順序どおりに呼ぶ.
        /// </summary>
        protected override void Shot(int numImages = 1, int interval = 0) {
            WaitPreviousSaveThread();
            PrepareShotSession();
            ResetShotQueue();
            StartSaveThread(numImages);
            try {
                WaitSaveThreadReady();
                BeginShot();
                RunShotLoop(numImages, interval);
            } finally {
                EndShotSession();
            }
        }

        protected override void BeginShot() {
            Debug.WriteLine("SelfTestCamera 撮影開始");
        }

        protected override Mat CaptureFrame() {
            return CreateFrame();
        }

        protected override void EndShot() {
            Debug.WriteLine("SelfTestCamera 撮影終了");
        }
    }
}
#endif