ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

工业相机应用开发:从图像采集到检测结果通信的工程化实践

工业相机应用开发:从图像采集到检测结果通信的工程化实践 一、工业相机程序不只是“采图加算法”在很多工业视觉项目中C 程序并不直接提供操作界面而是以后台进程、Windows 服务或 Linux 守护进程的形式运行。程序主要负责三件事对接工业相机稳定获取图像。调用视觉算法生成检测结果。通过 TCP、Modbus TCP、OPC UA 等常用协议将结果发送给上位机或控制系统。上位机负责参数配置、状态展示、报警显示、配方管理和生产数据记录C 相机程序则更接近一个独立的视觉服务。典型系统结构如下工业相机 ↓ C 图像采集程序 ↓ 图像预处理与视觉算法 ↓ 检测结果与运行状态 ↓ TCP / Modbus TCP / OPC UA / MQTT ↓ 上位机、PLC 或 MES在项目初期程序通常只需要完成以下流程打开相机 → 获取一张图像 → 执行算法 → 返回 OK 或 NG但进入长期运行阶段后经常会遇到下面这些问题相机断线后无法自动恢复。相机 SDK 回调线程被算法阻塞造成丢图。图像指针离开回调函数后失效。算法处理速度低于采集速度内存不断增长。硬件触发次数与收到的图像数量不一致。多台相机同时运行时图像和工件对应关系混乱。检测结果已经生成但上位机通信中断。上位机重复发送检测命令程序重复执行任务。通信恢复后不确定上一条结果是否已经被上位机接收。相机重连后曝光、增益和触发模式没有恢复。上位机下发错误参数导致相机进入异常状态。相机异常、算法异常和通信异常混在一起现场难以定位。因此工业相机程序不能只围绕 SDK 接口编写还需要建立完整的采集、处理、通信和异常恢复机制。一句话概括程序不仅要“得到检测结果”还要保证结果来自正确的图像并且可靠地交付给正确的上位机任务。二、程序的职责边界无界面的工业视觉程序通常承担以下职责枚举和识别工业相机。打开、关闭和配置相机。管理连续采集、软触发和硬件触发。接收和管理图像缓冲区。将图像投递到算法处理线程。生成检测结果。保存必要的原始图和结果图。管理相机断线和自动重连。对外提供设备状态和统计信息。接收上位机下发的检测命令和参数。将检测结果发送给上位机。记录运行日志、异常日志和通信日志。上位机通常负责操作界面。产品和配方选择。参数编辑。检测结果展示。报警展示。历史记录查询。用户权限。生产报表。与整机其他模块进行协调。需要明确的是C 相机程序不是上位机页面的一个附属线程而应作为相对独立的设备服务运行。即使上位机关闭或重启相机程序也应能够根据项目要求保持运行。停止新任务但保持连接。缓存未发送结果。等待上位机重新连接。对通信异常产生独立报警。三、推荐的工程结构以下以 C17、CMake 和 OpenCV 为例不包含 Qt 或其他界面框架。src/ VisionService/ main.cpp Application.cpp ServiceHost.cpp SignalHandler.cpp Core/ Interfaces/ ICamera.h ICameraFactory.h IInspectionPipeline.h IResultPublisher.h ICommandServer.h Models/ CameraInfo.h CameraConfig.h ImageFrame.h InspectionRequest.h InspectionResult.h ServiceStatus.h Common/ ErrorCode.h Result.h BlockingQueue.h ThreadSafeValue.h CameraDrivers/ VendorA/ VendorACamera.cpp VendorACamera.h VendorAFactory.cpp VendorB/ VendorBCamera.cpp VendorBCamera.h VendorBFactory.cpp Mock/ MockCamera.cpp MockCamera.h Acquisition/ CameraManager.cpp AcquisitionService.cpp TriggerCoordinator.cpp FrameMatcher.cpp CameraHealthMonitor.cpp Vision/ ImageConverter.cpp ImagePreprocessor.cpp InspectionPipeline.cpp AlgorithmManager.cpp CalibrationService.cpp Communication/ ProtocolManager.cpp Tcp/ TcpCommandServer.cpp TcpResultPublisher.cpp TcpMessageCodec.cpp Modbus/ ModbusTcpServer.cpp ModbusRegisterMap.cpp OpcUa/ OpcUaServer.cpp Mqtt/ MqttResultPublisher.cpp Infrastructure/ Configuration/ Logging/ ImageStorage/ Database/ Metrics/ tests/ CoreTests/ AcquisitionTests/ VisionTests/ CommunicationTests/程序内部推荐按照以下链路运行上位机命令 ↓ 通信协议适配层 ↓ 检测任务管理器 ↓ 相机触发与图像采集 ↓ 图像处理队列 ↓ 视觉算法 ↓ 检测结果 ↓ 通信协议适配层 ↓ 上位机相机驱动、算法模块和通信模块应相互隔离。算法模块不应直接调用相机 SDK通信模块也不应持有相机句柄。四、统一封装相机 SDK不同品牌工业相机的 SDK 差别较大但基本功能相似枚举设备。创建相机句柄。打开和关闭设备。设置曝光和增益。配置触发模式。开始和停止采集。获取图像。注册异常回调。建议先定义统一接口。#pragma once #include cstdint #include functional #include memory #include string #include vector enum class CameraState { Disconnected, Connecting, Connected, Grabbing, Reconnecting, Faulted }; enum class TriggerMode { Continuous, Software, Hardware }; enum class PixelFormat { Mono8, BayerRG8, BayerBG8, RGB8, BGR8, Unknown }; struct CameraInfo { std::string id; std::string name; std::string serialNumber; std::string modelName; std::string transportType; }; struct CameraConfig { double exposureUs 5000.0; double gain 0.0; double frameRate 20.0; TriggerMode triggerMode TriggerMode::Continuous; std::string triggerSource Line0; std::string triggerActivation RisingEdge; int acquisitionTimeoutMs 1000; }; struct ImageFrame { std::shared_ptrstd::uint8_t[] data; std::size_t dataSize 0; int width 0; int height 0; int stride 0; PixelFormat pixelFormat PixelFormat::Unknown; std::uint64_t frameId 0; std::uint64_t cameraTimestamp 0; std::string cameraId; }; using FrameCallback std::functionvoid(ImageFrame); class ICamera { public: virtual ~ICamera() default; virtual const CameraInfo info() const 0; virtual CameraState state() const 0; virtual void open() 0; virtual void close() noexcept 0; virtual void applyConfig( const CameraConfig config) 0; virtual CameraConfig readConfig() const 0; virtual void startGrabbing( FrameCallback callback) 0; virtual void stopGrabbing() noexcept 0; virtual void softwareTrigger() 0; virtual bool isDeviceAccessible() const 0; };业务层只依赖ICamera。如果后续更换相机品牌只需要新增对应的 SDK 适配器不需要重写检测流程和通信流程。五、使用 RAII 管理 SDK 资源工业相机 SDK 一般具有固定的资源使用顺序创建句柄 → 打开设备 → 配置参数 → 开始采集 → 停止采集 → 关闭设备 → 销毁句柄如果每个步骤都依赖手工释放程序异常退出时很容易出现资源泄漏。C 项目中应使用 RAII 管理相机句柄。class CameraHandle { public: CameraHandle() default; ~CameraHandle() { reset(); } CameraHandle(const CameraHandle) delete; CameraHandle operator(const CameraHandle) delete; CameraHandle(CameraHandle other) noexcept : handle_(other.handle_) { other.handle_ nullptr; } CameraHandle operator(CameraHandle other) noexcept { if (this ! other) { reset(); handle_ other.handle_; other.handle_ nullptr; } return *this; } void reset(void* newHandle nullptr) noexcept { if (handle_ ! nullptr) { // Vendor_DestroyHandle(handle_); } handle_ newHandle; } void* get() const noexcept { return handle_; } private: void* handle_ nullptr; };程序中的相机对象、线程、网络连接和文件句柄都应采用类似方式管理生命周期。六、图像内存必须明确所有权相机 SDK 返回的图像数据通常由 SDK 内部管理。常见情况包括图像指针只在回调函数内有效。下一帧到达后上一帧内存会被覆盖。图像缓冲区需要调用 SDK 接口归还。SDK 内部使用循环缓冲区。下面这种写法存在风险void onFrame( const unsigned char* data, int width, int height) { cv::Mat image( height, width, CV_8UC1, const_castunsigned char*(data)); frameQueue.push(image); }cv::Mat在这里没有复制图像数据。回调结束后image.data可能已经失效。如果图像需要交给其他线程处理应复制图像数据或者使用受控内存池。ImageFrame copyFrame( const std::uint8_t* source, std::size_t size, int width, int height, int stride, PixelFormat format, std::uint64_t frameId, const std::string cameraId) { auto buffer std::shared_ptrstd::uint8_t[]( new std::uint8_t[size], std::default_deletestd::uint8_t[]()); std::memcpy( buffer.get(), source, size); ImageFrame frame; frame.data std::move(buffer); frame.dataSize size; frame.width width; frame.height height; frame.stride stride; frame.pixelFormat format; frame.frameId frameId; frame.cameraId cameraId; return frame; }图像复制会增加内存带宽消耗但在项目初期优先保证数据正确更重要。后期如果图像较大、相机较多或帧率较高可以再使用固定内存池或 SDK 用户缓冲区机制进行优化。七、采集线程和算法线程必须分离相机 SDK 回调通常运行在厂商内部线程中。回调函数的原则是尽快完成图像接收然后立即返回。不应在采集回调中执行OpenCV 复杂处理。深度学习推理。图像保存。网络通信。数据库写入。等待上位机应答。大量日志输出。长时间持有互斥锁。推荐的回调流程收到图像 → 检查图像完整性 → 复制或接管图像内存 → 记录帧编号 → 放入图像队列 → 返回示例void AcquisitionService::onFrameReceived( const RawFrame rawFrame) { try { ImageFrame frame frameConverter_.copyFrom(rawFrame); if (!frameQueue_.tryPush(std::move(frame))) { statistics_.queueDropCount.fetch_add( 1, std::memory_order_relaxed); } statistics_.receivedFrameCount.fetch_add( 1, std::memory_order_relaxed); } catch (const std::exception ex) { logger_.error( 接收图像异常{}, ex.what()); } }算法线程独立消费队列void InspectionWorker::run() { while (!stopRequested_.load()) { auto frame frameQueue_.waitPop( std::chrono::milliseconds(200)); if (!frame.has_value()) { continue; } try { InspectionResult result pipeline_.execute(*frame); resultQueue_.push( std::move(result)); } catch (const std::exception ex) { logger_.error( 图像处理异常FrameId{}错误{}, frame-frameId, ex.what()); } } }结果通信同样建议使用独立线程避免网络发送阻塞算法线程。八、所有任务队列都必须有容量限制图像队列不能无限增长。假设相机每秒采集 30 帧而算法每秒只能处理 20 帧队列将以每秒 10 帧的速度增加。运行时间越长内存占用越大。图像处理延迟越长。检测结果与实际工件的位置偏差越大。最终可能导致程序崩溃。因此图像队列必须是有界队列。templatetypename T class BlockingQueue { public: explicit BlockingQueue(std::size_t capacity) : capacity_(capacity) { } bool tryPush(T value) { std::lock_guardstd::mutex lock(mutex_); if (stopped_ || queue_.size() capacity_) { return false; } queue_.push(std::move(value)); condition_.notify_one(); return true; } std::optionalT waitPop( std::chrono::milliseconds timeout) { std::unique_lockstd::mutex lock(mutex_); const bool ready condition_.wait_for( lock, timeout, [this] { return stopped_ || !queue_.empty(); }); if (!ready || queue_.empty()) { return std::nullopt; } T value std::move(queue_.front()); queue_.pop(); return value; } void stop() { std::lock_guardstd::mutex lock(mutex_); stopped_ true; condition_.notify_all(); } private: std::size_t capacity_; std::queueT queue_; std::mutex mutex_; std::condition_variable condition_; bool stopped_ false; };除图像队列外下面这些队列也应设置容量上限检测请求队列。算法任务队列。检测结果队列。图像保存队列。上位机消息发送队列。队列满时不能简单忽略应产生明确的错误码或报警。九、建立统一检测任务模型相机程序不能只返回一个简单的true或false。每次检测都应具有唯一任务编号用于关联上位机命令。相机触发。图像帧。算法结果。通信应答。保存的图像文件。检测请求模型示例struct InspectionRequest { std::uint64_t requestId 0; std::string productId; std::string recipeId; std::string stationId; std::string cameraId; std::uint64_t triggerSequence 0; int timeoutMs 2000; std::chrono::steady_clock::time_point createdAt; };检测结果模型示例enum class InspectionStatus { Success, ImageTimeout, CameraOffline, AlgorithmFailed, CommunicationFailed, Cancelled }; struct DefectItem { std::string code; std::string name; double score 0.0; int x 0; int y 0; int width 0; int height 0; }; struct InspectionResult { std::uint64_t requestId 0; std::uint64_t frameId 0; std::string productId; std::string recipeId; std::string cameraId; InspectionStatus status InspectionStatus::Success; bool passed false; std::string resultCode; std::string message; double processingTimeMs 0.0; std::vectorDefectItem defects; std::string originalImagePath; std::string resultImagePath; };典型流程如下上位机发送检测请求 → 程序生成或校验 RequestId → 检查相机状态和配方状态 → 执行软触发或等待硬件触发 → 接收图像 → 图像与 RequestId 匹配 → 执行算法 → 生成 InspectionResult → 发送结果 → 等待上位机确认十、检测命令必须具备幂等性上位机与相机程序之间发生通信超时时上位机可能重新发送同一条检测命令。如果程序没有重复命令识别机制同一个工件可能被检测两次。建议上位机为每个命令提供唯一序号{ messageType: inspection_request, sequence: 10251, requestId: 8800123, productId: P1008, recipeId: Recipe-A, cameraId: TopCamera }C 程序收到命令后应判断当前序号是否已经处理。当前requestId是否已经存在。是否已有相同任务正在执行。是否已经生成对应结果。对于重复请求可以返回之前的结果而不是再次触发相机。std::optionalInspectionResult InspectionHistory::findResult( std::uint64_t requestId) const { std::lock_guardstd::mutex lock(mutex_); auto iterator results_.find(requestId); if (iterator results_.end()) { return std::nullopt; } return iterator-second; }这类机制通常称为幂等处理是保证通信可靠性的关键。十一、设计统一的通信抽象层业务代码不应直接依赖某一种通信协议。可以定义统一的结果发布接口class IResultPublisher { public: virtual ~IResultPublisher() default; virtual bool isConnected() const 0; virtual void publishResult( const InspectionResult result) 0; virtual void publishStatus( const ServiceStatus status) 0; virtual void publishAlarm( const AlarmMessage alarm) 0; };上位机命令接收接口using InspectionRequestHandler std::functionvoid(InspectionRequest); using ConfigCommandHandler std::functionvoid(ConfigCommand); class ICommandServer { public: virtual ~ICommandServer() default; virtual void start() 0; virtual void stop() noexcept 0; virtual void setInspectionRequestHandler( InspectionRequestHandler handler) 0; virtual void setConfigCommandHandler( ConfigCommandHandler handler) 0; };不同协议分别实现这些接口TcpCommandServer ModbusTcpServer OpcUaServer MqttCommandSubscriber业务层只处理检测请求和检测结果不关心底层使用 TCP 还是 OPC UA。十二、自定义 TCP 协议设计自定义 TCP 是工业项目中较常见的通信方式。优点是实现相对直接。传输内容灵活。适合发送复杂检测结果。可以发送缺陷列表、坐标和文件路径。但 TCP 是字节流协议没有天然消息边界。不能假设一次send对应一次recv。推荐定义固定消息头#pragma pack(push, 1) struct MessageHeader { std::uint32_t magic 0x5649534E; std::uint16_t version 1; std::uint16_t messageType 0; std::uint32_t bodyLength 0; std::uint64_t sequence 0; std::uint32_t checksum 0; }; #pragma pack(pop)一个完整消息可以采用固定消息头 消息正文正文可以使用JSON。Protocol Buffers。MessagePack。自定义二进制结构。如果检测结果内容较复杂JSON 调试方便。示例{ messageType: inspection_result, sequence: 10252, requestId: 8800123, cameraId: TopCamera, status: success, passed: false, resultCode: NG_SCRATCH, processingTimeMs: 38.6, defects: [ { code: SCRATCH, score: 0.93, x: 520, y: 318, width: 80, height: 15 } ], imagePath: 2026-07-31/P1008/8800123.jpg }TCP 通信层至少应处理粘包和拆包。消息长度校验。协议版本。心跳。超时。断线重连。重复序号。消息校验。发送队列积压。上位机确认。十三、结果确认机制不能把send返回成功视为上位机已经收到并处理结果。完整结果流程应为检测结果生成 → 放入发送队列 → 通信线程发送 → 上位机收到结果 → 上位机返回 Ack → 程序标记结果已确认上位机确认消息示例{ messageType: result_ack, sequence: 10253, requestId: 8800123, accepted: true }等待确认期间程序应保留结果。struct PendingResult { InspectionResult result; int retryCount 0; std::chrono::steady_clock::time_point lastSentAt; };如果超时未收到确认可以按照策略重发第一次发送失败立即重试 第二次失败500 ms 后重试 第三次失败1 s 后重试 后续失败进入待恢复队列并产生通信报警重发时仍使用同一个requestId上位机也应进行幂等处理。十四、Modbus TCP 通信设计如果上位机或 PLC 主要使用寄存器交互可以采用 Modbus TCP。Modbus TCP 适合传输相机在线状态。程序运行状态。检测请求标志。检测完成标志。OK 或 NG。错误码。检测耗时。产品编号。任务序号。但不适合直接传输复杂缺陷列表和长字符串。可以设计如下寄存器映射地址名称方向说明40001CommandCode上位机→视觉程序命令编号40002CommandSequence上位机→视觉程序命令序号40003ProductCode上位机→视觉程序产品编号40004RecipeCode上位机→视觉程序配方编号40010AckSequence视觉程序→上位机已接收命令序号40011AckResult视觉程序→上位机接收结果40020ResultSequence视觉程序→上位机检测结果序号40021InspectionFinished视觉程序→上位机检测完成40022InspectionResult视觉程序→上位机0 未知、1 OK、2 NG40023ErrorCode视觉程序→上位机错误码40024ProcessingTime视觉程序→上位机检测耗时40030CameraState视觉程序→上位机相机状态40031ServiceHeartbeat视觉程序→上位机程序心跳Modbus 命令流程建议采用序号而不是只使用一个启动位。错误方式上位机将 Start 写为 1 → 视觉程序开始检测 → 视觉程序将 Start 清零该方式在网络异常时容易出现命令重复或命令丢失。推荐方式上位机写入 CommandCode → 上位机增加 CommandSequence → 视觉程序发现新的 CommandSequence → 视觉程序校验命令 → 视觉程序更新 AckSequence → 检测完成后更新 ResultSequence命令是否为新命令应通过序号判断。十五、OPC UA 与 MQTT 的适用场景OPC UAOPC UA 适合设备状态和结构化数据较多的项目。可以对外提供以下节点VisionService.Camera.TopCamera.State VisionService.Camera.TopCamera.FrameRate VisionService.Camera.TopCamera.Exposure VisionService.Inspection.CurrentRequestId VisionService.Inspection.LastResult VisionService.Inspection.LastErrorCode VisionService.Statistics.TotalCount VisionService.Statistics.OkCount VisionService.Statistics.NgCountOPC UA 的优点是数据结构清晰客户端可以订阅变量变化。如果项目已经有 OPC UA 基础设施使用 OPC UA 会比重新设计自定义协议更方便。MQTTMQTT 更适合将检测结果上传到服务器。与 MES 或数据平台交互。多设备集中监控。发布状态、报警和统计数据。主题可以设计为factory/line1/station2/vision/status factory/line1/station2/vision/result factory/line1/station2/vision/alarmMQTT 不建议直接承担严格实时的相机触发控制。对于实时检测命令通常仍采用 TCP、Modbus TCP 或现场总线MQTT 更适合数据上报。十六、相机参数由上位机下发时的处理流程上位机可能需要设置曝光时间。增益。触发模式。触发源。图像宽度和高度。ROI。算法阈值。配方编号。参数不能收到后立即写入相机。推荐流程接收参数命令 → 校验协议字段 → 校验参数范围 → 检查当前是否允许修改 → 暂停检测任务 → 停止相机采集 → 写入相机参数 → 回读相机参数 → 应用算法参数 → 恢复采集 → 返回参数应用结果参数命令模型struct CameraParameterCommand { std::uint64_t sequence 0; std::string cameraId; std::optionaldouble exposureUs; std::optionaldouble gain; std::optionaldouble frameRate; std::optionalTriggerMode triggerMode; std::optionalstd::string triggerSource; };返回结果struct CommandResult { std::uint64_t sequence 0; bool accepted false; bool finished false; int errorCode 0; std::string message; };参数写入后必须回读。部分相机参数会按照步进值自动调整因此应使用允许误差进行比较。十七、相机断线重连相机断线可能由以下原因造成相机掉电。网线松动。交换机重启。USB 接口异常。相机被其他程序占用。网络拥塞。SDK 内部异常。连接管理器应负责检查相机是否在线。检查是否长时间没有收到图像。停止采集。释放失效句柄。重新枚举相机。根据序列号查找目标设备。重新打开相机。恢复相机参数。恢复图像回调。重新开始采集。发布相机恢复状态。状态变化可以设计为Disconnected → Connecting → Connected → Grabbing → Reconnecting → Connected → Grabbing重连应采用退避策略第 1 次失败1 秒后重试 第 2 次失败2 秒后重试 第 3 次失败5 秒后重试 后续失败每 10 秒重试重连成功后不能直接继续之前的检测任务。断线前正在执行的任务应根据业务规则处理为相机离线失败。图像超时。任务取消。等待人工重新触发。不能把重连后的第一张图像错误关联到断线前的工件。十八、多相机设备管理多相机项目不能依赖枚举顺序识别设备。错误方式第 0 台设备 顶部相机 第 1 台设备 侧面相机设备重启后枚举顺序可能发生变化。推荐使用相机序列号作为硬件唯一标识。配置文件示例{ cameras: [ { role: TopCamera, serialNumber: CAM001234, enabled: true, configFile: top_camera.json }, { role: SideCamera, serialNumber: CAM005678, enabled: true, configFile: side_camera.json } ] }程序启动时按照序列号查找相机。每台相机应具有独立的相机对象。连接状态。图像队列。帧统计。采集线程。错误信息。重连任务。不要让所有相机共用一个大锁。一台相机异常不应阻塞其他相机。十九、图像与检测任务的匹配在硬件触发或多工位系统中不能简单认为“下一张图像就是当前任务的图像”。图像匹配可以结合任务编号。触发序号。图像帧编号。相机时间戳。上位机时间戳。PLC 工件编号。编码器位置。可以为每台相机维护等待图像的任务队列struct PendingCapture { InspectionRequest request; std::chrono::steady_clock::time_point deadline; };收到图像后由FrameMatcher完成匹配std::optionalInspectionRequest FrameMatcher::match( const ImageFrame frame) { std::lock_guardstd::mutex lock(mutex_); if (pendingRequests_.empty()) { return std::nullopt; } InspectionRequest request pendingRequests_.front(); pendingRequests_.pop(); return request; }上面的 FIFO 方式只适合严格顺序触发的场景。高速、多相机或可能乱序的系统应使用触发序号和时间戳进行匹配。二十、图像保存不能阻塞检测图像保存涉及图像格式转换。JPEG 或 PNG 编码。目录创建。磁盘写入。网络磁盘访问。这些操作耗时不稳定不能放在相机回调或算法主线程中。建议建立独立存图队列struct SaveImageTask { std::uint64_t requestId 0; cv::Mat image; std::filesystem::path filePath; bool isNgImage false; };图像保存策略应配置化保存所有原图。只保存 NG 图。保存 NG 原图和结果图。按比例抽样保存。调试模式保存全部图像。磁盘不足时停止保存。磁盘不足时继续检测但产生报警。按日期、产品和工位建立目录。建议目录结构images/ 2026-07-31/ Recipe-A/ OK/ NG/文件名应包含关键索引RequestId_CameraId_Result_Timestamp.jpg例如8800123_TopCamera_NG_20260731_102215381.jpg二十一、服务状态与心跳即使程序没有界面也必须对外提供运行状态。状态模型示例enum class ServiceState { Starting, Ready, Running, Degraded, Faulted, Stopping }; struct ServiceStatus { ServiceState state ServiceState::Starting; bool upperComputerConnected false; int onlineCameraCount 0; int configuredCameraCount 0; std::uint64_t totalInspectionCount 0; std::uint64_t okCount 0; std::uint64_t ngCount 0; int activeErrorCode 0; std::string activeErrorMessage; };上位机应能够获取程序是否启动。相机是否在线。相机是否正在采集。当前配方。当前任务编号。最近一次结果。最近一次错误。接收帧率。算法处理时间。图像队列长度。通信连接状态。心跳可以采用递增计数值VisionHeartbeat 1 VisionHeartbeat 2 VisionHeartbeat 3 ...相比固定写入1递增心跳更容易判断程序是否仍在运行。二十二、错误码应统一管理错误信息不能只依赖文本字符串。建议建立统一错误码enum class ErrorCode : int { Success 0, CameraNotFound 1001, CameraOpenFailed 1002, CameraDisconnected 1003, CameraConfigFailed 1004, ImageTimeout 2001, ImageIncomplete 2002, FrameQueueFull 2003, AlgorithmFailed 3001, AlgorithmTimeout 3002, RecipeLoadFailed 3003, ProtocolError 4001, UpperComputerOffline 4002, ResultAckTimeout 4003, InvalidCommand 5001, DuplicateCommand 5002, InvalidParameter 5003 };上位机根据错误码显示对应说明。错误记录至少包含错误码。错误名称。发生时间。相机编号。任务编号。当前状态。SDK 原始错误码。错误描述。建议处理方式。例如错误码2001 错误名称ImageTimeout 相机TopCamera 任务编号8800123 触发模式Hardware 等待时间1000 ms 最后帧号15822 建议检查外部触发信号、相机供电和网络连接二十三、后台服务的启动与停止程序没有界面后更要重视启动和退出顺序。推荐启动流程读取配置文件 → 初始化日志 → 初始化通信服务 → 枚举并打开相机 → 加载算法和配方 → 创建采集线程 → 创建算法线程 → 创建结果发送线程 → 启动健康监控 → 对外发布 Ready 状态推荐停止流程停止接收新命令 → 取消未开始任务 → 等待正在处理的任务结束 → 停止相机采集 → 停止算法线程 → 停止存图线程 → 发送最后状态 → 关闭通信连接 → 关闭相机 → 刷新日志 → 退出程序不要在收到退出信号后直接调用std::exit否则可能导致相机句柄未释放。日志没有写入磁盘。图像文件损坏。未确认结果丢失。上位机不知道服务已经停止。可以监听系统信号std::atomic_bool g_stopRequested false; void signalHandler(int) { g_stopRequested.store(true); } int main() { std::signal(SIGINT, signalHandler); std::signal(SIGTERM, signalHandler); Application app; app.start(); while (!g_stopRequested.load()) { std::this_thread::sleep_for( std::chrono::milliseconds(200)); } app.stop(); return 0; }在 Windows 中也可以进一步封装为 Windows Service在 Linux 中可以使用 systemd 管理程序。二十四、性能统计程序应持续统计以下数据相机实际接收帧率。图像不完整数量。相机 SDK 丢帧数量。软件队列丢帧数量。算法平均处理时间。算法最大处理时间。图像队列长度。结果队列长度。存图队列长度。上位机通信延迟。结果确认超时次数。相机重连次数。当前内存占用。建议区分不同阶段耗时命令接收时间 → 相机触发时间 → 图像到达时间 → 算法开始时间 → 算法结束时间 → 结果发送时间 → 上位机确认时间这样才能判断节拍问题发生在哪个环节。例如任务编号8800123 命令到触发2.1 ms 触发到收图18.4 ms 算法处理36.8 ms 结果编码0.6 ms 网络发送1.3 ms 上位机确认4.2 ms 总耗时63.4 ms二十五、常见问题与规避方式25.1 在相机回调中运行算法问题算法阻塞 SDK 采集线程造成图像缓存溢出。建议采集回调只投递图像算法独立运行。25.2 直接传递 SDK 图像指针问题图像数据可能被 SDK 回收或覆盖。建议复制图像或建立明确的缓冲区所有权机制。25.3 检测请求没有唯一编号问题通信重试时可能重复检测结果也无法准确对应工件。建议所有任务使用唯一requestId。25.4 把 TCP 发送成功当成结果交付成功问题只能说明数据进入本机网络栈不能证明上位机已处理。建议增加结果确认和超时重发机制。25.5 使用单一启动标志控制检测问题网络异常时容易重复触发或漏触发。建议使用命令序号和应答序号。25.6 相机断线后只重新打开设备问题相机参数和回调可能没有恢复。建议重新打开后完整恢复参数、回调和采集状态。25.7 多台相机依赖枚举顺序问题相机角色可能发生错位。建议使用序列号绑定业务角色。25.8 图像保存阻塞算法问题磁盘波动会直接影响检测节拍。建议使用独立存图队列和线程。25.9 通信线程直接操作相机问题协议处理和设备操作耦合后续难以维护。建议通信层只生成业务命令由任务服务执行。25.10 所有异常只返回一个 NG问题无法区分产品不合格和系统故障。建议区分正常 NG、采图失败、算法失败和通信失败。二十六、项目检查清单架构检查已建立统一相机接口。业务层不直接依赖相机 SDK。相机、算法和通信模块相互隔离。程序不依赖界面线程运行。支持 Windows 服务或 Linux 守护进程模式。支持模拟相机或离线图像测试。图像采集检查相机句柄使用 RAII 管理。图像数据所有权明确。采集回调中没有耗时业务。图像队列具有容量上限。能够检测图像超时。能够检测不完整图像。能够统计帧编号连续性。支持相机断线重连。检测任务检查每个任务具有唯一编号。图像能够关联对应任务。检测任务具有超时。重复命令能够识别。已完成任务结果能够查询。任务失败具有明确错误码。产品 NG 与程序异常能够区分。通信检查通信协议具有版本号。TCP 协议处理粘包和拆包。命令具有序号。结果具有序号。支持心跳。支持断线重连。结果发送后等待确认。结果确认超时能够重发。上位机重复确认不会产生异常。通信队列具有容量上限。参数检查参数具有范围校验。参数写入后执行回读。参数修改具有命令结果。运行中禁止修改危险参数。相机重连后恢复参数。配方版本可以追溯。配方切换失败时禁止继续检测。日志与统计检查记录程序启动和退出。记录相机连接和断开。记录上位机连接状态。记录命令序号和任务编号。记录算法处理时间。记录结果发送和确认。记录图像队列溢出。记录相机重连次数。日志能够按日期滚动。日志不会无限占用磁盘。二十七、小结无界面的 C 工业相机程序本质上是一个面向设备运行的视觉服务。它的重点不是提供图像显示窗口而是建立一套稳定的数据处理链路上位机命令 → 检测任务 → 相机触发 → 图像采集 → 算法处理 → 检测结果 → 通信发送 → 上位机确认项目中需要重点做好以下工作使用统一接口隔离相机厂商 SDK。使用 RAII 管理相机和通信资源。明确图像内存所有权。将采集、算法、存图和通信线程分开。使用有界队列控制内存和处理延迟。使用任务编号关联命令、图像和结果。使用命令序号解决重复请求问题。使用结果确认机制保证数据可靠交付。根据数据特点选择 TCP、Modbus TCP、OPC UA 或 MQTT。对相机断线和上位机断线分别进行恢复。用统一错误码区分相机、算法和通信故障。对处理时间、队列长度、帧率和通信延迟进行持续统计。当图像采集、检测任务和通信协议形成明确边界后程序才能从一个简单的相机 SDK 示例逐步变成可以长期运行、方便维护、能够接入整机系统的工业视觉服务。
返回列表