| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Diagnostics;
- using System.Linq;
- using System.Runtime.CompilerServices;
- using System.Threading;
- using System.Threading.Tasks;
- using TeamAAS.Camera.Enums;
- using TeamAAS.Camera.Images;
- using TeamAAS.Camera.Interfaces;
- namespace TeamAAS.Camera.Cameras
- {
- /// <summary>
- /// 相机基类:封装所有品牌相机的通用功能。
- /// 所有 SDK 操作均通过 _cameraLock(可重入读写锁)保护,防止多线程并发访问同一相机。
- /// </summary>
- public abstract class CameraBase : ICamera, INotifyPropertyChanged
- {
- #region 字段
- protected Thread m_hReceiveThread;
- protected bool _isHaveCamera = false;
- /// <summary>
- /// 相机操作互斥锁(可重入)。
- /// 所有与 SDK 的交互(开/关/取相/参数设置)必须在写锁内执行。
- /// </summary>
- private readonly ReaderWriterLockSlim _cameraLock =
- new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
- #endregion
- #region 基本信息
- private string _name;
- [Category("I.基本信息"), DisplayName("1.名称"), Description("自定义相机名称")]
- public string Name
- {
- get => _name;
- set => SetProperty(ref _name, value);
- }
- [Category("I.基本信息"), DisplayName("2.曝光时间(μs)"), Description("曝光时间,单位微秒")]
- public float ExposureTime
- {
- get => ReadParameter(GetExposureTime, ref _exposureTime);
- set => SetImageParameter(
- () => SetExposureTime(value),
- () => { _exposureTime = value; _hasExposure = true; },
- value, $"曝光时间 {value}μs", nameof(ExposureTime));
- }
- [Category("I.基本信息"), DisplayName("3.增益(亮度)"), Description("增益越高画面越亮,噪点也随之增大")]
- public float Gain
- {
- get => ReadParameter(GetGain, ref _gain);
- set => SetImageParameter(
- () => SetGain(value),
- () => { _gain = value; _hasGain = true; },
- value, $"增益 {value}", nameof(Gain));
- }
- [Category("I.基本信息"), DisplayName("4.Gamma"), Description("Gamma 校正系数,默认 1.0 表示不校正")]
- public float Gamma
- {
- get => ReadParameter(GetGamma, ref _gamma);
- set => SetImageParameter(
- () => SetGamma(value),
- () => { UpdateGammaSnapshot(value); _hasGamma = true; },
- value, $"Gamma {value}", nameof(Gamma));
- }
- #endregion
- #region 触发模式
- /// <summary>所有相机都具备的最小能力集:跟随相机(不下发)+ 连续采集。</summary>
- protected static readonly IReadOnlyList<CameraTriggerMode> ContinuousOnlyTriggerModes =
- new[] { CameraTriggerMode.跟随相机, CameraTriggerMode.连续采集 };
- /// <summary>全功能触发模式集,供支持软/硬触发的工业相机直接复用。</summary>
- protected static readonly IReadOnlyList<CameraTriggerMode> FullTriggerModes =
- new[]
- {
- CameraTriggerMode.跟随相机,
- CameraTriggerMode.连续采集,
- CameraTriggerMode.软触发,
- CameraTriggerMode.硬触发上升沿,
- CameraTriggerMode.硬触发下降沿,
- };
- /// <summary>
- /// 本相机支持的触发模式(默认仅“跟随相机/连续采集”)。
- /// 子类按自家 SDK 能力重写:支持软/硬触发的直接返回 <see cref="FullTriggerModes"/>;
- /// 无触发能力的(如 UVC 网络摄像头)保持默认即可——上层下拉列表会随之增减。
- /// </summary>
- [Browsable(false)]
- public virtual IReadOnlyList<CameraTriggerMode> SupportedTriggerModes => ContinuousOnlyTriggerModes;
- /// <summary>
- /// 相机连接后的默认触发模式。子类若原先在 OpenDevice 里硬编码了触发设置,
- /// 必须在此返回等价模式,以保证既有行为不变(如 Basler/OPT 原为软触发)。
- /// </summary>
- protected virtual CameraTriggerMode DefaultTriggerMode => CameraTriggerMode.跟随相机;
- private CameraTriggerMode _triggerMode = CameraTriggerMode.跟随相机;
- /// <summary>当前生效的触发模式</summary>
- [Browsable(false)]
- public CameraTriggerMode TriggerMode
- {
- get => _triggerMode;
- protected set => SetProperty(ref _triggerMode, value);
- }
- /// <summary>当前是否软触发:子类 Grab 据此决定是否发 TriggerSoftware 命令</summary>
- [Browsable(false)]
- public bool IsSoftwareTrigger => _triggerMode == CameraTriggerMode.软触发;
- /// <summary>当前是否外部硬触发:取相需等待外部信号,无信号会超时</summary>
- [Browsable(false)]
- public bool IsExternalTrigger =>
- _triggerMode == CameraTriggerMode.硬触发上升沿 ||
- _triggerMode == CameraTriggerMode.硬触发下降沿;
- /// <summary>该相机是否支持指定触发模式(“跟随相机”恒支持,因为它不下发任何设置)</summary>
- public bool SupportsTriggerMode(CameraTriggerMode mode)
- => mode == CameraTriggerMode.跟随相机 ||
- (SupportedTriggerModes != null && SupportedTriggerModes.Contains(mode));
- /// <summary>
- /// 设置触发模式:先按 <see cref="SupportedTriggerModes"/> 校验能力,再交子类下发硬件。
- /// 无论下发成败都落地期望值快照(相机未连接时由 OpenDevice 补下发)。
- /// </summary>
- public virtual bool SetTriggerMode(CameraTriggerMode mode)
- {
- // “跟随相机”= 不接管触发设置,保持相机当前生效模式。
- // 必须原封不动返回:若把快照改成“跟随相机”,软触发相机会停发 TriggerSoftware
- // 而硬件仍在 TriggerMode=On 等触发,导致取相一直超时卡死。
- if (mode == CameraTriggerMode.跟随相机)
- {
- ErrorMessage = null;
- return true;
- }
- if (!SupportsTriggerMode(mode))
- {
- ErrorMessage = $"{Name} 不支持触发模式「{mode}」(支持:{string.Join("、", SupportedTriggerModes ?? ContinuousOnlyTriggerModes)})";
- return false;
- }
- try
- {
- bool ok = ApplyTriggerMode(mode);
- ErrorMessage = ok ? null : $"设置触发模式「{mode}」失败(若相机未连接,连接后将自动应用)";
- return ok;
- }
- catch (Exception ex)
- {
- ErrorMessage = $"设置触发模式异常: {ex.Message}";
- return false;
- }
- finally
- {
- TriggerMode = mode;
- }
- }
- /// <summary>
- /// 子类实现:把触发模式下发到 SDK。基类默认空实现(仅记录快照),适用于无触发能力的相机。
- /// 若 SDK 调用需要互斥,实现内自行用 <see cref="WriteLockScope"/>(锁可重入)。
- /// </summary>
- protected virtual bool ApplyTriggerMode(CameraTriggerMode mode) => true;
- #endregion
- #region 公共属性
- public abstract string CameraBrand { get; }
- [Browsable(false)]
- public Guid ID { get; protected set; }
- [Browsable(false)]
- public int Index { get; set; }
- [Category("杂项"), DisplayName("1.厂商"), ReadOnly(true)]
- public string ManufacturerName { get; protected set; }
- [Category("杂项"), DisplayName("2.型号"), ReadOnly(true)]
- public string ModelName { get; protected set; }
- [Category("杂项"), DisplayName("3.序列号"), ReadOnly(true)]
- public string SerialNumber { get; protected set; }
- [Category("杂项"), DisplayName("4.IP地址"), ReadOnly(true)]
- public string CameraIp { get; protected set; }
- private bool _isConnected;
- [Category("杂项"), DisplayName("5.连接状态"), ReadOnly(true)]
- public bool IsConnected
- {
- get { return _isConnected; }
- protected set { SetProperty(ref _isConnected, value); }
- }
- #endregion
- #region Orthor
- [Browsable(false)]
- public bool IsFindByIp { get; protected set; }
- private bool _isGrabbing;
- [Browsable(false)]
- public bool IsGrabbing
- {
- get { return _isGrabbing; }
- protected set { SetProperty(ref _isGrabbing, value); }
- }
- private GenImage _image;
- [Browsable(false)]
- public GenImage Image
- {
- get { return _image; }
- protected set { SetProperty(ref _image, value); }
- }
- [Browsable(false)]
- public CameraType CameraType { get; protected set; }
- [Browsable(false)]
- public UInt32 ImageWidth { get; protected set; }
- [Browsable(false)]
- public UInt32 ImageHeight { get; protected set; }
- [Browsable(false)]
- public TimeSpan TotalTime { get; protected set; }
- [Browsable(false)]
- public string ErrorMessage { get; protected set; }
- #endregion
- #region 事件
- /// <summary>
- /// 取相
- /// </summary>
- public event Action<GenImage, TimeSpan, string> ImageCallbackEvent;
- public event Action<Guid, bool> CameraConnectChangedEvent;
- #endregion
- #region 构造函数
- protected CameraBase(Guid id, int index, string name, string serialNumber, string cameraIp, bool isFindByIp)
- {
- ID = id;
- Name = name;
- Index = index;
- CameraIp = cameraIp;
- SerialNumber = serialNumber;
- IsFindByIp = isFindByIp;
- IsConnected = false;
- // 按子类声明的默认触发模式初始化快照(等价于原来 OpenDevice 里的硬编码设置)
- _triggerMode = DefaultTriggerMode;
- }
- #endregion
- #region 线程同步辅助
- /// <summary>
- /// 获取写锁作用域 — 用于所有 SDK 写操作(开/关/取相/参数设置)。
- /// 使用方式:using (WriteLockScope()) { ... }
- /// </summary>
- protected IDisposable WriteLockScope()
- {
- _cameraLock.EnterWriteLock();
- return new WriteLockDisposable(_cameraLock);
- }
- /// <summary>
- /// 获取读锁作用域 — 用于只读 SDK 操作(获取参数值)。
- /// </summary>
- protected IDisposable ReadLockScope()
- {
- _cameraLock.EnterReadLock();
- return new ReadLockDisposable(_cameraLock);
- }
- /// <summary>
- /// 获取写锁(无 using 模式时使用,需手动调用 WriteUnlock)。
- /// </summary>
- protected void WriteLock()
- {
- _cameraLock.EnterWriteLock();
- }
- /// <summary>
- /// 释放写锁。
- /// </summary>
- protected void WriteUnlock()
- {
- _cameraLock.ExitWriteLock();
- }
- /// <summary>
- /// 获取读锁(无 using 模式时使用,需手动调用 ReadUnlock)。
- /// </summary>
- protected void ReadLock()
- {
- _cameraLock.EnterReadLock();
- }
- /// <summary>
- /// 释放读锁。
- /// </summary>
- protected void ReadUnlock()
- {
- _cameraLock.ExitReadLock();
- }
- private sealed class WriteLockDisposable : IDisposable
- {
- private readonly ReaderWriterLockSlim _lock;
- public WriteLockDisposable(ReaderWriterLockSlim rwLock) { _lock = rwLock; }
- public void Dispose() { _lock.ExitWriteLock(); }
- }
- private sealed class ReadLockDisposable : IDisposable
- {
- private readonly ReaderWriterLockSlim _lock;
- public ReadLockDisposable(ReaderWriterLockSlim rwLock) { _lock = rwLock; }
- public void Dispose() { _lock.ExitReadLock(); }
- }
- #endregion
- #region 通用实现(不需要子类重写)
- private int _readingParameters;
- private float _exposureTime;
- private float _gain;
- private float _gamma = 1.0f;
- // 用户是否显式设置过对应参数(含从配置加载)。只有设置过才会在连接/采集时自动下发,
- // 避免把默认值(曝光 0、增益 0)强行推给硬件。
- private bool _hasExposure;
- private bool _hasGain;
- private bool _hasGamma;
- /// <summary>
- /// 参数写入统一路径:先尝试下发硬件(无论成败),再把期望值落地到本地快照。
- /// 失败/异常只记 ErrorMessage 并返回——相机未连接时用户设置的值不会丢,
- /// 连接后会经 TryApplyImageParameters 自动应用。
- /// </summary>
- private void SetImageParameter(Func<bool> apply, Action updateSnapshot, float value, string label, string notifyProperty)
- {
- try
- {
- bool ok = apply();
- ErrorMessage = ok ? null : $"设置{label}失败(若相机未连接,连接后将自动应用)";
- }
- catch (Exception ex)
- {
- ErrorMessage = $"设置{label}异常: {ex.Message}";
- }
- finally
- {
- updateSnapshot();
- }
- RaisePropertyChanged(notifyProperty);
- }
- /// <summary>
- /// 把本地保存的期望参数值推送到硬件(幂等,可重复调用)。
- /// 触发时机:连接成功(OpenDeviceAsync)、开始采集(StartGrabbing)、配置初始化后首连。
- /// 仅推送用户显式设置过的参数,避免把默认值强写到硬件。
- /// </summary>
- public void TryApplyImageParameters()
- {
- if (!IsConnected) return;
- // 自动曝光是相机品牌特有(UVC)的运行时开关,不在这里处理;
- // 各品牌的 SetExposureTime/SetGain 会自行判断是否处于自动模式,并据此忽略手动写入。
- if (_hasExposure) { try { SetExposureTime(_exposureTime); } catch { } }
- if (_hasGain) { try { SetGain(_gain); } catch { } }
- if (_hasGamma) { try { SetGamma(_gamma); } catch { } }
- }
- /// <summary>
- /// 从 SDK 读取参数值并同步到快照。
- /// 重入防护:子类 GetXxx 的兜底若再读本属性(_readingParameters > 0),直接返回快照阻断递归;
- /// SDK 异常(未连接等)同样回退快照。计数为实例级、非原子 —— 本属性仅由 UI/业务线程按序访问。
- /// </summary>
- private float ReadParameter(Func<float> read, ref float snapshot)
- {
- if (_readingParameters > 0) return snapshot;
- _readingParameters++;
- try { snapshot = read(); return snapshot; }
- catch { return snapshot; }
- finally { _readingParameters--; }
- }
- /// <summary>
- /// 仅更新本地 Gamma 快照并通知 UI(不写硬件),供基类默认实现与子类兜底路径使用。
- /// 注意:子类内部请勿写 "Gamma = x"——属性 setter 会再次进入 SetGamma 造成无限递归。
- /// </summary>
- protected void UpdateGammaSnapshot(float gamma)
- {
- SetProperty(ref _gamma, gamma);
- }
- /// <summary>
- /// 打开相机异步(通用实现,包装 OpenDevice)。
- /// 打开成功后自动把本地保存的参数(曝光/增益/Gamma)下发到硬件。
- /// </summary>
- public Task<bool> OpenDeviceAsync()
- {
- return Task.Run(() =>
- {
- bool opened = OpenDevice();
- if (opened) TryApplyImageParameters();
- return opened;
- });
- }
- /// <summary>
- /// 开始实时采集图像(通用线程管理)。启动前把待应用的参数快照推送到硬件。
- /// </summary>
- public virtual void StartGrabbing()
- {
- if (IsConnected) TryApplyImageParameters();
- using (WriteLockScope())
- {
- if (IsGrabbing) return;
- IsGrabbing = true;
- m_hReceiveThread = new Thread(GetStreamThreadProc) { IsBackground = true };
- m_hReceiveThread.Start();
- }
- }
- /// <summary>
- /// 停止实时采集图像(通用线程管理)
- /// </summary>
- public virtual void StopGrabbing()
- {
- using (WriteLockScope())
- {
- try
- {
- IsGrabbing = false;
- if (m_hReceiveThread != null && m_hReceiveThread.IsAlive)
- {
- m_hReceiveThread.Abort();
- m_hReceiveThread = null;
- }
- }
- catch { }
- }
- }
- /// <summary>
- /// 实时采集线程过程(通用实现)
- /// </summary>
- protected virtual void GetStreamThreadProc()
- {
- while (IsGrabbing)
- {
- try
- {
- Grab();
- ImageCallbackEvent?.Invoke(Image, TotalTime, ErrorMessage);
- }
- catch { }
- Thread.Sleep(10);
- }
- IsGrabbing = false;
- }
- /// <summary>
- /// 批量设置相机参数(子类可重写以支持更多参数)
- /// </summary>
- public virtual void ApplyParameters(Dictionary<string, object> parameters)
- {
- if (parameters == null) return;
- using (WriteLockScope())
- {
- foreach (var kv in parameters)
- {
- try
- {
- switch (kv.Key.ToLower())
- {
- case "exposure":
- case "exposuretime":
- if (kv.Value is float f1) SetExposureTime(f1);
- else if (kv.Value is double d1) SetExposureTime((float)d1);
- break;
- case "gain":
- if (kv.Value is float f2) SetGain(f2);
- else if (kv.Value is double d2) SetGain((float)d2);
- break;
- case "gamma":
- if (kv.Value is float f3) SetGamma(f3);
- else if (kv.Value is double d3) SetGamma((float)d3);
- break;
- }
- }
- catch { }
- }
- }
- }
- /// <summary>
- /// 获取当前相机参数快照
- /// </summary>
- public virtual Dictionary<string, object> GetParameters()
- {
- var dict = new Dictionary<string, object>();
- using (ReadLockScope())
- {
- try { dict["ExposureTime"] = GetExposureTime(); } catch { }
- try { dict["Gain"] = GetGain(); } catch { }
- try { dict["Gamma"] = GetGamma(); } catch { }
- }
- return dict;
- }
- #endregion
- #region 保护方法
- /// <summary>
- /// 触发连接状态变更事件
- /// </summary>
- protected void OnConnectChanged(bool connected)
- {
- CameraConnectChangedEvent?.Invoke(ID, connected);
- }
- /// <summary>
- /// 设置错误信息并返回 null
- /// </summary>
- protected GenImage SetError(string message)
- {
- ErrorMessage = message;
- return null;
- }
- /// <summary>
- /// 对图像应用当前 Gamma 校正(软件后处理)。
- /// 子类应在 Grab() 成功获取图像后调用此方法。
- /// </summary>
- protected void ApplyGammaToImage(GenImage image)
- {
- if (image == null) return;
- if (Math.Abs(_gamma - 1.0f) < 0.001f) return;
- image.ApplyGamma(_gamma);
- image.SetMetadata("Gamma", _gamma);
- }
- /// <summary>
- /// 终处理:对图像应用 Gamma、写入 Metadata,返回处理后的图像。
- /// 子类应在 Grab() 成功获取图像后调用。
- /// </summary>
- protected GenImage FinalizeImage(GenImage image)
- {
- if (image == null) return null;
- ApplyGammaToImage(image);
- image.SetMetadata("CameraName", Name);
- image.SetMetadata("Timestamp", DateTime.Now);
- return image;
- }
- #endregion
- #region 抽象方法(子类必须实现,必须在方法体开头使用 WriteLockScope/ReadLockScope)
- /// <summary>打开相机</summary>
- public abstract bool OpenDevice();
- /// <summary>关闭相机</summary>
- public abstract void CloseDevice();
- /// <summary>采集单张图片,返回通用 GenImage</summary>
- public abstract GenImage Grab();
- /// <summary>设置曝光时间</summary>
- public abstract bool SetExposureTime(float exposureTime);
- /// <summary>获取曝光时间</summary>
- public abstract float GetExposureTime();
- /// <summary>设置增益</summary>
- public abstract bool SetGain(float gain);
- /// <summary>获取增益</summary>
- public abstract float GetGain();
- /// <summary>
- /// 设置 Gamma 值(默认实现为软件后处理,子类可重写以支持硬件 Gamma)。
- /// 实现内部只允许通过 <see cref="UpdateGammaSnapshot"/> 更新快照,禁止给 Gamma 属性赋值(会递归)。
- /// </summary>
- public virtual bool SetGamma(float gamma)
- {
- UpdateGammaSnapshot(gamma);
- return true;
- }
- /// <summary>
- /// 获取当前 Gamma 值(直接返回本地快照字段,不经过 Gamma 属性——避免与属性 getter 互相递归)
- /// </summary>
- public virtual float GetGamma() => _gamma;
- #endregion
- #region INotifyPropertyChanged
- public event PropertyChangedEventHandler PropertyChanged;
- protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
- {
- if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
- storage = value;
- RaisePropertyChanged(propertyName);
- return true;
- }
- protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
- {
- if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
- storage = value;
- onChanged?.Invoke();
- RaisePropertyChanged(propertyName);
- return true;
- }
- protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
- {
- OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
- }
- protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
- {
- PropertyChanged?.Invoke(this, args);
- }
- #endregion
- }
- }
|