#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