工业相机接入
大约 8 分钟约 2483 字
工业相机接入
简介
工业相机是机器视觉和自动化检测的核心硬件。WPF 上位机通过相机 SDK(海康、大华、基恩士等)或通用协议(GigE Vision、USB3 Vision)接入工业相机。掌握图像采集、回调处理和显示优化,是实现视觉检测、尺寸测量、缺陷检测等功能的基础。
特点
通用相机接口
接口设计
/// <summary>
/// 通用工业相机接口
/// </summary>
public interface IIndustrialCamera : IDisposable
{
string CameraName { get; }
bool IsConnected { get; }
Task<bool> ConnectAsync();
Task DisconnectAsync();
// 参数设置
void SetExposureTime(double microseconds);
void SetGain(double gain);
void SetTriggerMode(TriggerMode mode);
// 采集控制
Task StartContinuousAsync();
Task StopAsync();
Task<CameraImage?> SingleCaptureAsync();
// 图像回调
event Action<CameraImage>? ImageReceived;
event Action<string>? ErrorOccurred;
}
public enum TriggerMode
{
Continuous, // 连续采集
SoftwareTrigger, // 软触发
HardwareTrigger // 硬触发(外部信号)
}
public class CameraImage
{
public byte[] Data { get; set; } = Array.Empty<byte>();
public int Width { get; set; }
public int Height { get; set; }
public int Channels { get; set; }
public long Timestamp { get; set; }
public long FrameNumber { get; set; }
}海康相机封装
HikCamera 实现
/// <summary>
/// 海康威视工业相机封装
/// </summary>
public class HikCamera : IIndustrialCamera
{
private MyCamera? _camera;
private MyCamera.MV_CC_DEVICE_INFO? _deviceInfo;
public string CameraName { get; private set; } = "";
public bool IsConnected { get; private set; }
public event Action<CameraImage>? ImageReceived;
public event Action<string>? ErrorOccurred;
public async Task<bool> ConnectAsync()
{
_camera = new MyCamera();
// 枚举设备
var deviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST();
var ret = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE, ref deviceList);
if (ret != MyCamera.MV_OK || deviceList.nDeviceNum == 0)
{
ErrorOccurred?.Invoke("未找到相机设备");
return false;
}
// 打开第一个设备
_deviceInfo = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(
deviceList.pDeviceInfo[0], typeof(MyCamera.MV_CC_DEVICE_INFO));
ret = _camera.MV_CC_CreateDevice_NET(ref _deviceInfo.Value);
ret = _camera.MV_CC_OpenDevice_NET();
if (ret != MyCamera.MV_OK)
{
ErrorOccurred?.Invoke($"打开设备失败:{ret}");
return false;
}
// 注册图像回调
_camera.MV_CC_RegisterImageCallBack_NET(ImageCallback, IntPtr.Zero);
IsConnected = true;
CameraName = _deviceInfo.Value.chUserDefinedName;
return true;
}
// 图像回调
private void ImageCallback(ref MyCamera.MV_FRAME_OUT_INFO frameInfo, IntPtr pData, IntPtr pUser)
{
try
{
var imageData = new byte[frameInfo.nFrameLen];
Marshal.Copy(pData, imageData, 0, (int)frameInfo.nFrameLen);
var image = new CameraImage
{
Data = imageData,
Width = (int)frameInfo.nWidth,
Height = (int)frameInfo.nHeight,
Channels = 1, // 工业相机通常为单通道灰度
FrameNumber = (long)frameInfo.nFrameNum,
Timestamp = (long)frameInfo.nHostTimeStamp
};
ImageReceived?.Invoke(image);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"图像回调异常:{ex.Message}");
}
}
public void SetExposureTime(double microseconds)
{
_camera?.MV_CC_SetFloatValue_NET("ExposureTime", (float)microseconds);
}
public void SetGain(double gain)
{
_camera?.MV_CC_SetFloatValue_NET("Gain", (float)gain);
}
public void SetTriggerMode(TriggerMode mode)
{
if (_camera == null) return;
switch (mode)
{
case TriggerMode.Continuous:
_camera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
break;
case TriggerMode.SoftwareTrigger:
_camera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_TRIGGER_MODE.MV_TRIGGER_MODE_ON);
_camera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE);
break;
case TriggerMode.HardwareTrigger:
_camera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_TRIGGER_MODE.MV_TRIGGER_MODE_ON);
_camera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0);
break;
}
}
public async Task StartContinuousAsync()
{
SetTriggerMode(TriggerMode.Continuous);
_camera?.MV_CC_StartGrabbing_NET();
}
public async Task StopAsync()
{
_camera?.MV_CC_StopGrabbing_NET();
}
public async Task<CameraImage?> SingleCaptureAsync()
{
SetTriggerMode(TriggerMode.SoftwareTrigger);
_camera?.MV_CC_StartGrabbing_NET();
_camera?.MV_CC_SetCommandValue_NET("TriggerSoftware");
// 等待图像回调
var tcs = new TaskCompletionSource<CameraImage?>();
void handler(CameraImage img) => tcs.TrySetResult(img);
ImageReceived += handler;
var result = await tcs.Task;
ImageReceived -= handler;
_camera?.MV_CC_StopGrabbing_NET();
return result;
}
public async Task DisconnectAsync()
{
await StopAsync();
_camera?.MV_CC_CloseDevice_NET();
_camera?.MV_CC_DestroyDevice_NET();
IsConnected = false;
}
public void Dispose()
{
_ = DisconnectAsync();
}
}相机管理器
多相机管理
/// <summary>
/// 多相机管理器 — 统一管理多台工业相机
/// </summary>
public class CameraManager : ObservableObject, IDisposable
{
private readonly Dictionary<string, IIndustrialCamera> _cameras = new();
private readonly Dictionary<string, CameraDisplayService> _displays = new();
private readonly ILogger<CameraManager> _logger;
[ObservableProperty]
private ObservableCollection<CameraInfo> _cameraList = new();
[ObservableProperty]
private CameraInfo? _selectedCamera;
public event Action<CameraImage, string>? ImageCaptured;
public CameraManager(ILogger<CameraManager> logger)
{
_logger = logger;
}
// 枚举所有可用相机
public List<CameraInfo> EnumerateCameras()
{
var cameras = new List<CameraInfo>();
// 海康相机枚举
try
{
var deviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST();
MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList);
for (int i = 0; i < deviceList.nDeviceNum; i++)
{
var info = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(
deviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO));
cameras.Add(new CameraInfo
{
Index = i,
Name = info.chUserDefinedName,
SerialNumber = info.chSerialNumber,
Model = info.chModelName,
Type = info.nTLayerType == MyCamera.MV_GIGE_DEVICE ? "GigE" : "USB3"
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "枚举相机失败");
}
CameraList = new ObservableCollection<CameraInfo>(cameras);
return cameras;
}
// 连接指定相机
public async Task<bool> ConnectCameraAsync(CameraInfo info, Dispatcher dispatcher)
{
try
{
var camera = new HikCamera();
var connected = await camera.ConnectAsync();
if (connected)
{
_cameras[info.SerialNumber] = camera;
var display = new CameraDisplayService(dispatcher);
camera.ImageReceived += (img) =>
{
display.UpdateImage(img);
ImageCaptured?.Invoke(img, info.SerialNumber);
};
_displays[info.SerialNumber] = display;
}
return connected;
}
catch (Exception ex)
{
_logger.LogError(ex, "连接相机 {Name} 失败", info.Name);
return false;
}
}
// 获取相机显示 Bitmap
public WriteableBitmap? GetBitmap(string serialNumber)
{
return _displays.TryGetValue(serialNumber, out var display) ? display.Bitmap : null;
}
public void Dispose()
{
foreach (var camera in _cameras.Values)
camera.Dispose();
_cameras.Clear();
}
}
public class CameraInfo
{
public int Index { get; set; }
public string Name { get; set; } = "";
public string SerialNumber { get; set; } = "";
public string Model { get; set; } = "";
public string Type { get; set; } = "";
public override string ToString() => $"{Name} ({Type})";
}触发模式详解
多种触发场景
/// <summary>
/// 相机触发控制服务
/// </summary>
public class CameraTriggerService
{
private readonly IIndustrialCamera _camera;
public CameraTriggerService(IIndustrialCamera camera)
{
_camera = camera;
}
// 连续采集 — 用于实时监控
public async Task StartContinuousCaptureAsync()
{
_camera.SetTriggerMode(TriggerMode.Continuous);
await _camera.StartContinuousAsync();
}
// 软触发 — 用于手动拍照或流程控制
public async Task<CameraImage?> SoftwareTriggerAsync(int timeoutMs = 3000)
{
_camera.SetTriggerMode(TriggerMode.SoftwareTrigger);
await _camera.StartContinuousAsync();
// 通过信号量等待回调
var tcs = new TaskCompletionSource<CameraImage?>();
CameraImage? captured = null;
void handler(CameraImage img)
{
captured = img;
tcs.TrySetResult(img);
}
_camera.ImageReceived += handler;
// 发送软触发命令
// 具体命令依赖 SDK
var completed = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs));
_camera.ImageReceived -= handler;
await _camera.StopAsync();
return completed == tcs.Task ? captured : null;
}
// 硬触发 — 配合 PLC / 传感器
public async Task SetupHardwareTriggerAsync(int triggerSourceLine = 0)
{
_camera.SetTriggerMode(TriggerMode.HardwareTrigger);
// 配置触发源(具体 API 依赖相机厂商 SDK)
// 海康示例:
// _camera.SetEnumValue("TriggerSource", triggerSourceLine);
// _camera.SetEnumValue("TriggerActivation", "RisingEdge");
await _camera.StartContinuousAsync();
// 相机将等待外部信号触发采集
}
// 限速采集 — 控制采集帧率
public async Task StartRateLimitedCaptureAsync(double fps)
{
_camera.SetTriggerMode(TriggerMode.Continuous);
// 设置采集帧率
// 海康:_camera.SetFloatValue("AcquisitionFrameRate", fps);
// 启用帧率控制:_camera.SetEnumValue("AcquisitionFrameRateEnable", true);
await _camera.StartContinuousAsync();
}
}图像处理集成
图像采集后处理
/// <summary>
/// 图像处理管线 — 采集 → 预处理 → 检测 → 显示
/// </summary>
public class ImageProcessingPipeline
{
private readonly Action<CameraImage> _onProcessed;
private readonly int _maxQueueSize = 10;
private readonly BlockingCollection<CameraImage> _queue = new();
public ImageProcessingPipeline(Action<CameraImage> onProcessed)
{
_onProcessed = onProcessed;
StartProcessing();
}
// 图像入队(回调线程调用)
public void Enqueue(CameraImage image)
{
if (_queue.Count >= _maxQueueSize)
{
// 队列满时丢弃最旧的帧
_queue.TryTake(out _);
}
_queue.Add(image);
}
private void StartProcessing()
{
Task.Run(() =>
{
foreach (var image in _queue.GetConsumingEnumerable())
{
try
{
var processed = ProcessImage(image);
_onProcessed(processed);
}
catch (Exception ex)
{
// 记录处理异常但不中断管线
}
}
});
}
private CameraImage ProcessImage(CameraImage image)
{
// 灰度转换(如果原图是彩色)
byte[] processedData;
if (image.Channels == 3)
{
// RGB 转灰度
processedData = new byte[image.Width * image.Height];
for (int i = 0; i < processedData.Length; i++)
{
int r = image.Data[i * 3];
int g = image.Data[i * 3 + 1];
int b = image.Data[i * 3 + 2];
processedData[i] = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
}
}
else
{
processedData = image.Data;
}
return new CameraImage
{
Data = processedData,
Width = image.Width,
Height = image.Height,
Channels = 1,
Timestamp = image.Timestamp,
FrameNumber = image.FrameNumber
};
}
public void Stop()
{
_queue.CompleteAdding();
}
}图像显示优化
高性能图像渲染
/// <summary>
/// 相机图像高性能显示
/// </summary>
public class CameraDisplayService
{
private WriteableBitmap? _writeableBitmap;
private readonly Dispatcher _dispatcher;
private int _skipCount;
private const int SkipThreshold = 2; // 跳帧显示
public CameraDisplayService(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public WriteableBitmap? Bitmap => _writeableBitmap;
public void Initialize(int width, int height)
{
_writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Gray8, null);
}
public void UpdateImage(CameraImage image)
{
// 跳帧处理 — UI 来不及刷新时跳过部分帧
if (++_skipCount < SkipThreshold) return;
_skipCount = 0;
_dispatcher.BeginInvoke(() =>
{
if (_writeableBitmap == null) return;
try
{
_writeableBitmap.Lock();
_writeableBitmap.WritePixels(
new Int32Rect(0, 0, image.Width, image.Height),
image.Data,
image.Width, // stride
0);
_writeableBitmap.Unlock();
}
catch { }
}, DispatcherPriority.Render);
}
}<!-- XAML 显示 -->
<Image Source="{Binding CameraDisplay.Bitmap}"
Stretch="Uniform"
RenderOptions.BitmapScalingMode="NearestNeighbor"/>优点
缺点
总结
工业相机接入核心:定义通用 IIndustrialCamera 接口 → 按厂商 SDK 封装实现 → 图像回调获取帧数据 → WriteableBitmap 高性能显示。触发模式选择:连续监控用 Continuous,配合 PLC 用 HardwareTrigger,单次拍照用 SoftwareTrigger。显示优化:跳帧 + DispatcherPriority.Render。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《工业相机接入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《工业相机接入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《工业相机接入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《工业相机接入》最大的收益和代价分别是什么?
