ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

海康工业三相机联动串口触发系统:从 0 到 1 的踩坑笔记

海康工业三相机联动串口触发系统:从 0 到 1 的踩坑笔记 作者小小张日期2025-07-24 这篇博文不是“Hello World”而是一篇“Hello 三相机 串口 回调 外部触发”的笔记。 如果你也正好被“海康工业相机 Python 多相机同步 串口发字符”的组合折磨过希望下面的 3000 字能帮你少掉几根头发。## 1. 需求背景一句话讲清楚 **“电脑连 3 台海康相机USB/GiGE 都行只要相机被外部信号触发一次就立刻通过串口往外发一个字符3 台相机互不干扰。”**听起来简单过程却踩了 5 个大坑1. 海康官方 Python Demo 只有单相机。2. Windows 下 Python 多相机回调容易“串台”。3. 官方 SDK 的“外部触发”必须**先关闭取流**才能配置否则报错。4. USB 相机与 GiGE 相机 API 混用时枚举顺序每次启动都可能变。5. 串口句柄必须**在回调线程里重新判空**否则偶发 OSError: exception: access violation。---## 2. 最终效果运行脚本后控制台出现串口就绪!!找到:3个相机usb相机:0 相机名:正相机 相机序列号: 18137521usb相机:1 相机名:反相机 相机序列号: 18137635usb相机:2 相机名:带相机 相机序列号: 18138184默认0/1/2代表 正/反/三 [3个相机] 如正确 输入一个数字[按回车]:按回车后3 台相机同时开始取流并进入回调模式。此时用 PLC / 按钮 / 信号发生器给相机触发一次终端立刻出现1 M1 m1 O同时串口 COM3 会依次吐出字符 M、m、O对应 0/1/2 号相机。按任意键退出相机会优雅地关闭。---## 3. 关键实现拆解### 3.1 三相机枚举 序列号绑定海康的 Python SDK 枚举结果顺序**不固定**用序列号硬绑定最稳pythoncam1_nConnectionNum next(i for i in range(deviceList.nDeviceNum)if 18137521 in str(deviceList.pDeviceInfo[i]))cam2_nConnectionNum ...### 3.2 注册回调的正确姿势官方 Demo 里 RegisterImageCallBackEx 只能注册一次多相机必须**每实例注册一次**pythonCALL_BACK_0 FrameInfoCallBack(image_callback_0)CALL_BACK_1 FrameInfoCallBack(image_callback_1)CALL_BACK_2 FrameInfoCallBack(image_callback_2)cam.MV_CC_RegisterImageCallBackEx(CALL_BACK_0, None)cam2.MV_CC_RegisterImageCallBackEx(CALL_BACK_1, None)cam3.MV_CC_RegisterImageCallBackEx(CALL_BACK_2, None)### 3.3 触发模式配置顺序必须先 MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_OFF) 把触发关掉再设置 TriggerSource, TriggerActivation 等参数最后 MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_ON)。否则设置会失败返回 0x80000010参数无效。### 3.4 串口线程安全回调函数里直接 ser.write(...) 偶尔会崩加锁 判空pythondef Ser_send(send_data):if ser and ser.is_open:try:ser.write(send_data.encode())except Exception as e:print(串口写入失败, e)---## 4. 完整代码可直接运行# -- coding: utf-8 -- r 实现功能: 打开3个海康工业相机 回调模式 支持 网口gige USB 进入回调后就串口输出字符 支持外部触发 备注:3个相机和1个串口同时打开才能正常运行 名称:小小张 python3.8 thonny import random import os,sys,time,msvcrt from ctypes import * #主要靠的ctypes 导C语言的包 sys.path.append(os.getenv(MVCAM_COMMON_RUNENV) /Samples/Python/MvImport) #运行环境依赖海康的python包 mvs4.5.1安装后 可独立运行 from MvCameraControl_class import * R_list[M,N,m,n,O,S] # 打开 COM3波特率 1152008 数据位无校验1 停止位超时 0.5 秒 import serial serserial.Serial(COM3,115200,bytesizeserial.EIGHTBITS,parityserial.PARITY_NONE,stopbitsserial.STOPBITS_ONE,timeout0.5) #winsows系统使用com连接串行口 if (ser.isOpen()):print(串口就绪!!) def port_close(): ser.close() if (ser.isOpen()): print(关闭失败) def Ser_send(send_data): if (ser.isOpen()): ser.write(send_data.encode(utf-8)) #utf-8 编码发送 #ser.write(binascii.a2b_hex(send_data)) #Hex发送 return send_data r flag_a False flag_b False flag_c False # 函数 A每次调用将 flag_a 取反 #【新增三相机回调函数定义】 winfun_ctype WINFUNCTYPE stFrameInfo POINTER(MV_FRAME_OUT_INFO_EX) pData POINTER(c_ubyte) FrameInfoCallBack winfun_ctype(None, pData, stFrameInfo, c_void_p) #C指针转成 py对 # 0 号相机的回调 def image_callback_0(pData, pFrameInfo, pUser): global flag_a flag_a not flag_a stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(M)} ) # 1 号相机的回调 def image_callback_1(pData, pFrameInfo, pUser): global flag_b flag_b not flag_b stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(m)} ) #random.choice([O, S]) # 2 号相机的回调 def image_callback_2(pData, pFrameInfo, pUser): global flag_c flag_c not flag_c stFrameInfo cast(pFrameInfo, POINTER(MV_FRAME_OUT_INFO_EX)).contents if stFrameInfo: print(f {stFrameInfo.nFrameNum1} {Ser_send(O)} ) CALL_BACK_0 FrameInfoCallBack(image_callback_0) CALL_BACK_1 FrameInfoCallBack(image_callback_1) CALL_BACK_2 FrameInfoCallBack(image_callback_2) # if __name__ __main__: # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() deviceList MV_CC_DEVICE_INFO_LIST() tlayerType (MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE) # ch:枚举设备 | en:Enum device ret MvCamera.MV_CC_EnumDevices(tlayerType, deviceList) if ret ! 0: print(枚举设备0个) if deviceList.nDeviceNum 0: input(枚举设备0个:) print (f找到:{deviceList.nDeviceNum}个相机) for i in range(0, deviceList.nDeviceNum): mvcc_dev_info cast(deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents if mvcc_dev_info.nTLayerType MV_GIGE_DEVICE or mvcc_dev_info.nTLayerType MV_GENTL_GIGE_DEVICE: print (\ngige device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) nip1 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0xff000000) 24) nip2 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x00ff0000) 16) nip3 ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x0000ff00) 8) nip4 (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp 0x000000ff) print (current ip: %d.%d.%d.%d\n % (nip1, nip2, nip3, nip4)) elif mvcc_dev_info.nTLayerType MV_USB_DEVICE:# 这里usb 开始 print (fusb相机:{i}) strModeName for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (f相机名:{strModeName}) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (f相机序列号: {strSerialNumber}) pass# usb 在这 elif mvcc_dev_info.nTLayerType MV_GENTL_CAMERALINK_DEVICE: print (\nCML device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) elif mvcc_dev_info.nTLayerType MV_GENTL_CXP_DEVICE: print (\nCXP device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) elif mvcc_dev_info.nTLayerType MV_GENTL_XOF_DEVICE: print (\nXoF device: [%d] % i) strModeName for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chModelName: if per 0: break strModeName strModeName chr(per) print (device model name: %s % strModeName) strSerialNumber for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chSerialNumber: if per 0: break strSerialNumber strSerialNumber chr(per) print (user serial number: %s % strSerialNumber) print (f默认0/1/2代表 正/反/三 [3个相机] 如正确 输入一个数字[按回车]:) print (f不正确 请修改顺序) nConnectionNum input(默认0/1/2代表 正/反/三 [3个相机] 如果正确 输入0[按回车]:) if int(nConnectionNum) deviceList.nDeviceNum: print (intput error!) # ch:创建相机实例 | en:Creat Camera Object cam MvCamera();cam2 MvCamera();cam3 MvCamera() r 串口就绪!! 找到:3个相机 usb相机:0 相机名:正相机 相机序列号: 18137521 usb相机:1 相机名:反相机 相机序列号: 18137635 usb相机:2 相机名:带相机 相机序列号: 18138184 相机名:正相机0 相机名:反相机1 相机名:带相机2 根据情况修改下面3个参数 从而实现相机对应 cam1_nConnectionNum0 cam2_nConnectionNum1 cam3_nConnectionNum2 # ch:选择设备并创建句柄 | en:Select device and create handle stDeviceList cast(deviceList.pDeviceInfo[int(cam1_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam.MV_CC_CreateHandle(stDeviceList) stDeviceList2 cast(deviceList.pDeviceInfo[int(cam2_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam2.MV_CC_CreateHandle(stDeviceList2) stDeviceList3 cast(deviceList.pDeviceInfo[int(cam3_nConnectionNum)], POINTER(MV_CC_DEVICE_INFO)).contents ret cam3.MV_CC_CreateHandle(stDeviceList3) #【新增打开三相机】 ret cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f1相机已经打开) ret cam2.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f2相机已经打开) ret cam3.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0) if ret ! 0: print (f3相机已经打开) # # ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera) if stDeviceList.nTLayerType MV_GIGE_DEVICE or stDeviceList.nTLayerType MV_GENTL_GIGE_DEVICE: nPacketSize cam.MV_CC_GetOptimalPacketSize() if int(nPacketSize) 0: ret cam.MV_CC_SetIntValue(GevSCPSPacketSize,nPacketSize) if ret ! 0: print (Warning: Set Packet Size fail! ret[0x%x] % ret) else: print (Warning: Get Packet Size fail! ret[0x%x] % nPacketSize) r input(任意键继续) # ch:设置触发模式为off | en:Set trigger mode as off #设置触发模式 #设置 触发极性 #设置 触发 消抖时间 #设置 相机的相关参数 # 设置曝光# 设置增益 exposureTime130;gain11; fValue 30; for h in [cam, cam2, cam3]: h.MV_CC_SetEnumValue(TriggerMode, MV_TRIGGER_MODE_OFF)#3相机触发模式 h.MV_CC_SetBoolValue(TriggerCacheEnable, True);# 凭感觉 打开这个 可以缓存一个触发信号 h.MV_CC_SetFloatValue(ExposureTime, float(exposureTime)) h.MV_CC_SetFloatValue(Gain, float(gain)) h.MV_CC_SetFloatValue(AcquisitionFrameRate, float(fValue));#设置相机30帧 #h.MV_CC_SetEnumValue(TriggerMode, 1)#触发模式打开 #h.MV_CC_SetEnumValue(TriggerSource, 7)#触发线路Line0 #h.MV_CC_SetEnumValue(TriggerActivation, 0)#触发极性 上升沿 #参数 采集控制 r 开硬件触发 虚拟相机肯定不进回调 #【新增为三相机分别注册回调】 cam.MV_CC_RegisterImageCallBackEx(CALL_BACK_0, None) cam2.MV_CC_RegisterImageCallBackEx(CALL_BACK_1, None) cam3.MV_CC_RegisterImageCallBackEx(CALL_BACK_2, None) # #【新增三相机同时开始取流】 cam.MV_CC_StartGrabbing();cam2.MV_CC_StartGrabbing();cam3.MV_CC_StartGrabbing() # # 只要不开始取流 mvs可以设置的参数都可以设 # 打开相机后停止取流状态可以mvs可以设置的参数都可以设 # 有些参数 开始取流 或取流过程中也可以设置 # 读取参数 只要相机打开就能读. print (按任意键 停止取流!) msvcrt.getch()# # 阻塞 #【新增三相机同时停止取流】 cam.MV_CC_StopGrabbing();cam2.MV_CC_StopGrabbing();cam3.MV_CC_StopGrabbing() # #【新增三相机同时关闭并销毁】 cam.MV_CC_CloseDevice();cam2.MV_CC_CloseDevice();cam3.MV_CC_CloseDevice() cam.MV_CC_DestroyHandle();cam2.MV_CC_DestroyHandle();cam3.MV_CC_DestroyHandle() # MvCamera.MV_CC_Finalize()# ch:反初始化SDK---## 5. FAQ| 问题 | 解决思路 ||---|---|| **枚举不到 USB 相机** | 确认已安装 MVS 4.5.1并在“设备管理器”里能看到 Hikrobot Industrial Camera。 || **回调不触发** | 先确认相机真的被外部触发MVS 客户端里能看到帧计数再检查 TriggerSource 是否和硬件接线一致。 || **串口发字符乱码** | ser.write(M.encode(utf-8)) 用 utf-8 编码接收端也请用相同编码。 || **退出时卡死** | 先 StopGrabbing 再 CloseDevice顺序反了会阻塞。 |---## 6. 下一步可以做什么- 把 M/m/O 换成 JSON带上时间戳方便上位机解析。- 用 asyncio pyserial-asyncio 做异步串口减少阻塞。- 把触发信号改成软触发用 Python 直接 MV_CC_SetCommandValue(TriggerSoftware) 做时序同步。--- 如果文章帮到了你欢迎点个 ⭐Star 或留言交流 也欢迎把需求扔给我。
返回列表