海康工业三相机联动串口触发系统:从 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 相机名:正相机 相机序列号: 18137521

usb相机:1 相机名:反相机 相机序列号: 18137635

usb相机:2 相机名:带相机 相机序列号: 18138184

默认0/1/2代表 正/反/三 [3个相机] 如正确 输入一个数字[按回车]:

```

按回车后,3 台相机同时开始取流并进入回调模式。

此时用 PLC / 按钮 / 信号发生器给相机触发一次,终端立刻出现:

```

1 M

1 m

1 O

```

同时串口 `COM3` 会依次吐出字符 `M`、`m`、`O`(对应 0/1/2 号相机)。

按任意键退出,相机会优雅地关闭。


3. 关键实现拆解

3.1 三相机枚举 & 序列号绑定

海康的 Python SDK 枚举结果顺序**不固定**,用序列号硬绑定最稳:

```python

cam1_nConnectionNum = next(i for i in range(deviceList.nDeviceNum)

if "18137521" in str(deviceList.pDeviceInfo[i]))

cam2_nConnectionNum = ...

```

3.2 注册回调的正确姿势

官方 Demo 里 `RegisterImageCallBackEx` 只能注册一次,多相机必须**每实例注册一次**:

```python

CALL_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(...)` 偶尔会崩,加锁 + 判空:

```python

def Ser_send(send_data):

if ser and ser.is_open:

try:

ser.write(send_data.encode())

except Exception as e:

print("串口写入失败", e)

```


4. 完整代码(可直接运行)

python 复制代码
# -- 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,波特率 115200,8 数据位,无校验,1 停止位,超时 0.5 秒
import serial
ser=serial.Serial("COM3",115200,bytesize=serial.EIGHTBITS,parity=serial.PARITY_NONE,stopbits=serial.STOPBITS_ONE,timeout=0.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.nFrameNum+1} {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.nFrameNum+1} {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.nFrameNum+1} {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 (f"usb相机:{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_nConnectionNum=0
    cam2_nConnectionNum=1
    cam3_nConnectionNum=2
    # 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 (f"1相机已经打开")
    ret = cam2.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
    if ret != 0:
        print (f"2相机已经打开")
    ret = cam3.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
    if ret != 0:
        print (f"3相机已经打开")
    #=============================================================
    
    
    


    # 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
    #设置触发模式
    #设置 触发极性
    #设置 触发 消抖时间
    #设置 相机的相关参数
    # 设置曝光# 设置增益
    exposureTime=130;gain=11; 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 或留言交流!

> 也欢迎把需求扔给我。

相关推荐
停走的风2 小时前
Yolo底层原理学习(V1~V3)(第一篇)
人工智能·深度学习·神经网络·学习·yolo
小关会打代码2 小时前
Python编程进阶知识之第五课处理数据(matplotlib)
开发语言·python·机器学习·matplotlib·绘图
青云交5 小时前
Java 大视界 -- Java 大数据机器学习模型在金融衍生品市场波动特征挖掘与交易策略创新中的应用(363)
java·大数据·机器学习·量化交易·金融衍生品·交易策略·波动率预测
cwn_10 小时前
Sequential 损失函数 反向传播 优化器 模型的使用修改保存加载
人工智能·pytorch·python·深度学习·机器学习
lucky_lyovo12 小时前
循环神经网络--LSTM模型
rnn·机器学习·lstm
go&Python14 小时前
scikit-learn 包
python·机器学习·scikit-learn
sssammmm14 小时前
AI入门学习-Python 最主流的机器学习库Scikit-learn
人工智能·python·机器学习
Cedric111316 小时前
显微科研中的关键选择:不同显微镜相机技术特性与应用适配性全面解析
数码相机
北京地铁1号线17 小时前
YOLO12论文阅读:Attention-Centric Real-Time Object Detectors
论文阅读·yolo·目标检测
过往入尘土17 小时前
机器学习的基础知识
机器学习