
简介面向机器视觉开发者与相机驱动工程师这是一份基于GigE Vision协议的源码资源包用于学习相机通过以太网传输图像、控制参数及处理网络数据包的实现方式可应用于工业自动化、科研成像和交通监控等场景。压缩包共1个文件仅含一个XML配置文件整体约485KB用于定义虚拟GigE相机在缺少实体相机的条件下可据此模拟图像采集流程进行协议调试、功能验证与教学演示。目前已有1413人学习浏览。借助这份源码和配置示例读者可以研究GigE Vision 2.0带来的新特性梳理相机枚举、初始化、图像抓取、参数配置和虚拟相机调用等开发要点也能结合协议机制理解网络传输中的错误处理与带宽优化思路为后续集成真实GigE相机或构建图像处理系统打下基础。1. 拿到GigE Vision源码包后最难的不是读代码而是重建协议现场GigE Vision源码.rar 这个包在工业视觉圈里出现的频率很高但多数人在解压之后会卡在同一个地方代码能编译却不知道程序为什么连不上相机。GigE Vision协议本质上是一套基于UDP的工业相机通信标准它把相机当成一个挂在网卡上的设备但不是一个普通IP设备。它靠广播发现相机、靠专用端口发控制命令、靠另一条动态端口把图像数据一帧一帧砸过来。只要这三段链路里任何一段没对上程序就会表现得非常诡异有时能找到相机打不开有时打开了就是拿不到图有时拿到图了却断断续续。这篇文章就围绕这个源码包展开。我会先从协议骨架说起再带你按源码包的文件结构梳理模块分工然后给出一套从发现设备到抓取第一帧的最小可运行流程。后面几章专门写参数怎么调、哪些环节最容易翻车。内容是按一线调试思路写的适合正在自研相机接入模块的开发者也适合那些拿到源码需要改协议细节、做深度定制的工程师。新手可以照着命令一步步跑通熟手可以直接跳到避坑章节对号入座。2. 先看懂协议栈再读源码GigE Vision的发现、控制与流传输三块骨架2.1 发现机制设备搜出来不是靠Ping而是靠UDP广播GigE Vision设备发现用的是UDP广播目标端口固定为3956广播地址一般是255.255.255.255。相机收到广播包后会回给发送方一个设备确认报文里面带相机IP、网卡MAC、设备型号名、制造商信息、流通道数量等关键字段。这个机制决定了源码里任何「枚举设备」的功能都不可能靠TCP连接或ICMP来实现。常见的源码包里设备发现模块通常长这样构造一个Discover包包的头部有固定魔数、消息类型、标志位和长度字段包体里还可以带请求的设备范围。发送广播后用一个带超时的recvfrom循环等回复。这个循环是新手最容易写崩的地方。因为广播是异步的相机可能在几百微秒到几毫秒内回到也可能因为网络延迟超过100ms后才到。很多实现直接用了一个2s的阻塞收包这没问题但有些实现把socket超时设得太短比如200ms结果在真实网段里就是时不时找不到设备。我一般会这样写发现部分的骨架import socket import struct import time GVCP_PORT 3956 DISCOVERY_CMD 0x0001 # Discovery命令 # 构造GigE Vision Discovery报文 header struct.pack(HHHH, 0x42, DISCOVERY_CMD, 1, 0) # 0x42是GVCP协议魔数的低位 # 第二个字段是命令类型1代表Discovery # 第三个字段是序列号每次发送递增 # 第四个字段是标志位通常置0 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.bind((, GVCP_PORT)) sock.settimeout(2.0) sock.sendto(header, (255.255.255.255, GVCP_PORT)) devices [] end_time time.time() 2.0 while time.time() end_time: try: data, addr sock.recvfrom(2048) except socket.timeout: break if len(data) 24: continue # 校验魔数 magic, ack_id struct.unpack(HH, data[:4]) if magic ! 0x42: continue devices.append({ip: addr[0], raw: data})这段逻辑说明几个关键点。socket必须bind到3956端口因为相机的回复目标是发送端的随机端口如果你不bind到固定端口很多相机会把回复发到临时端口上然后你的代码就再也等不到这条消息。sendto的目标端口是3956这是所有GigE Vision设备默认监听的控制通道端口。SO_BROADCAST选项必须开否则广播包会被内核直接丢弃。while循环里的结束条件用绝对时间而不是在每轮迭代中重置超时这样可避免相机回复慢导致收集窗口被无限拉长。发现报文里的ack_id字段往往被忽略但实际调试时它非常有用。每次发送Discovery命令时序列号递增回复里的ack_id如果和你发出的不同说明代码在并发场景下收到了过期回复或串包。源码包里如果看到有人用全局变量存序列号说明设计上已经考虑过多设备并发发现的场景。2.2 控制通道GVCP不是HTTP那样的请求响应模型GVCPGigE Vision Control Protocol走UDP端口还是3956。控制通道用来读写相机寄存器、获取设备信息、配置图像大小和像素格式、开始和停止采集。它不是流式的长连接每次操作都是一问一答的报文交互。这里有个很容易误解的点很多人一看到「控制」就以为是TCP或者某种可靠传输实际上GVCP没有重传机制的设计保证超时和丢包全靠应用层自己兜底。源码包里的寄存器读写实现基本就是围绕ReadReg和WriteReg两条命令展开。一条命令的包结构固定是头部加请求地址和长度回复里则带上寄存器值或错误码。如果某一台相机对某些只读寄存器执行了写操作它会返回ACCESS_DENIED之类的错误码如果你把地址长度设成4字节以外的值大部分相机会返回INVALID_PARAMETER。这些错误码定义在源码包里通常都有一个枚举文件统一管理值得先翻出来看一遍。选型上的考虑是读寄存器要比读XML描述文件快得多但寄存器地址需要查相机的用户手册。自研SDK时我建议最少实现四条GVCP命令DISCOVERY、READREG、WRITEREG和CONTROL_CHANNEL_PRIVILEGE。第四条容易被忽略但在和某些相机配合时会遇到相机在非控制模式下拒绝执行任何改变采集状态的写寄存器指令必须先抢占控制通道的权限。权限之争在多进程访问同一台相机时就会出现所以源码包里如果写了权限处理逻辑一定要保留。很多开源实现里只实现了读写和采集控制结果两台电脑同时连一台相机时后连的那台就会时不时把前一台的采集搞挂。2.3 流传输通道GVSP的包结构决定了丢包后怎么处理GVSPGigE Vision Streaming Protocol是图像数据的传输协议目标端口不固定。相机在开始采集之前会通过控制通道告诉你流通道要发往哪个IP和端口。这个端口是相机自己分配的不是代码里随便定的。任何写死的目标端口都不可靠源码包里所有的流通道初始化最终都要以相机返回的SCPSPacketSize和SCPSDoNotFragment这类参数为准。GVSP数据包有明确的类型字段。类型0xC1是数据帧的第一个包0xC2是中间包0xC3是最后一包。一个完整的图像帧由同一block_id的所有包组成。block_id不是从1开始的简单递加在连续采集模式下它会一直累加重启采集后可能从任意值开始。因此你判断丢帧不能只看block_id是否为上一帧1而是要看同一block_id内有没有收齐头、中间、尾三类包。如果一帧只有头包和尾包但中间的0xC2数量不对那帧数据就是碎的强行拼出来一定是花屏或者错位图像。源码包里GVSP接收部分一般设计成一个独立线程收到包后按block_id写入对应的帧缓冲区。缓冲区不足时会触发拷贝策略常见的有丢新保旧和丢旧保新两种。采集延迟要求严格的场景建议丢旧保新这样应用层拿到的总是最新的完整帧代价是偶尔会漏帧而需要帧率稳定的场景建议丢新保旧保证每帧都能被应用读取代价是延迟会逐渐增大。源码里的FrameBufferSize如果小于一整帧的字节数会连带触发截断逻辑这部分在日志里通常表现为帧头正常但图像尾部全黑。3. 拆解源码包的结构先分清设备管理、流接收和XML描述三个目录再看代码3.1 一个典型GigE Vision源码包的文件组织方式虽然不同的包拆分习惯不完全一样但大体上会按功能切成交互层、协议层和平台适配层。交互层负责给应用提供打开关闭设备、设置参数、取图等接口协议层实现GVCP和GVSP报文编解码平台适配层处理网卡参数、多线程、操作系统差异。常见目录结构大致是gige_vision_src/ ├── device/ # 设备管理与枚举 │ ├── discover.py │ ├── control.py │ └── stream.py ├── protocol/ # GVCP/GVSP报文封装 │ ├── gvcp_packet.py │ ├── gvsp_packet.py │ └── gvcp_defs.py # 错误码与寄存器地址定义 ├── genicam/ # GenICam XML解析 │ ├── camera_xml_parser.py │ └── node_map.py ├── platform/ # 网卡与系统适配 │ ├── nic_config.py │ └── thread_utils.py └── main.py # 入口与调用示例拿到源码包先读protocol目录里的常量定义文件因为所有命令类型、错误码、标志位的含义都在这里。很多包会把错误码描述得不直观比如0xF001代表INVALID_HEADER你如果没看定义就去调试会对着十六进制数字一头雾水。然后看device/stream.py这是接收图像的核心帧分配、重组、丢帧判断都在这。最后再看genicam里的XML解析因为相机参数节点可以动态扩展不同相机厂商的节点差异很大源码包里如果只做了静态映射换一台相机就很容易报参数找不到。有个常见的整体性误判是把整个源码包当成一个可以直接pip安装的SDK来用。实际上多数源码包里没有完整的XML配置相机型号一换节点映射就崩了。遇到这种情况我一般会先用相机厂商提供的SDK导出相机描述文件再用源码包里自己的XML解析器去读那个描述文件。这样既能保留源码包里的协议实现又能兼容多型号相机。3.2 设备枚举与打开第一步就是在网卡列表里找到正确的接口源码包里的设备枚举模块通常不只是发一个广播那么简单。它还要搜主机所有非回环网卡IP并逐个判断是否和相机IP同网段。不同网段时即使能收到相机的广播回复后续建立流通道也大概率失败因为相机回复的数据包会跑到默认网关去绕一圈。在Linux下我会先做一次网卡巡检ip -4 addr show | grep inet # 观察每张网卡的IP与掩码确认相机所在网段是否被直接路由覆盖 # 如果相机在192.168.0.x而网卡是192.168.1.x就需要先重新配IP这个检查看起来多余恰恰是源码包跑不起来的大部分原因。工业相机通常默认IP是192.168.0.100左右但宿主机网卡可能自动获取到了172.16.x.x的地址双方根本不在一个广播域里。更隐蔽的情况是网卡启用了DHCP相机也启用了DHCP两者段的分配结果不稳定今天能连上明天就连不上。源码里写死IP当然不优雅但相机和PC的网卡IP都手动设为固定值是调试GigE视线的第一玄学。还有一个容易被忽略的细节多网卡机器上向255.255.255.255发广播回复可能被网卡驱动路由到错误的接口上从而导致相机永远「发现不了」。稳妥做法是先枚举网卡IP列表逐接口发一次Discovery广播然后从回复里的相机MAC与接口对应关系反推相机挂在哪个网卡上。源码包里如果没做逐接口广播你就得自己在主循环外加一层网卡遍历。3.3 把XML节点映射读懂你才知道寄存器值改的是哪个功能GenICam标准把相机参数组织成一颗节点树每个节点有一个名字和一个类型类型可以是整数、浮点数、枚举、布尔或字符串。相机启动时会把这张描述表放在内存里上位机通过读寄存器的方式把它抓回来用XML文本形式呈现。源码包里的解析器任务就是把XML转成活的数据结构让你能通过相机实例.曝光时间 1000这样的方式改参数。解析器最容易出错的地方是整数缩放。很多相机的曝光时间寄存器内部以微秒为单位直接取值但也有相机内部是14位精度需要把真实值除以或乘以一个系数再写入寄存器。XML里通常定义INCREMENT步进值和MIN/MAX范围源码里如果没有做这一步换算设置值就会表现得很奇怪比如写1000相机实际应用到的是10000或者永远是最近的一个离散档位。在调试源码包时我推荐把节点树先打印出来看一遍from genicam.node_map import NodeMap node_map NodeMap() node_map.load_from_xml(/path/to/camera_description.xml) # 打印所有节点名和类型确认过滤层没把关键参数丢掉 for node in node_map.nodes(): print(node.name, node.display_name, node.type_name)这样做的价值在于你会立刻发现源码包内置的节点列表和你手里这台相机的描述文件不一致这种不一致是很多「参数改不动」「曝光调了没反应」问题的根源。如果在源码包里找不到对应节点就回去检查XML解析器是否只处理了固定命名空间有些相机厂商的节点名前缀很长不匹配就会被跳过。规范的解析器应该按name精确匹配而不是按子字符串模糊匹配。模糊匹配会把ExposureTimeAbs和ExposureTimeRaw混为一谈导致代码在不知不觉中写给了错误的节点。4. 跑通最小采集流程从发现相机到拿到第一帧完整图4.1 采集前的准备清单网卡、防火墙、巨型帧三项检查在写任何采集代码之前先把宿主机环境检查一遍。实测中最容易卡住的位置是防火墙和巨型帧开关这两个因素都不出现在源码的报错信息里但它们都会让GVSP数据包在进入应用前就被丢弃。网卡设置我一般这样处理sudo ethtool -s eth0 speed 1000 duplex full autoneg off # 强制千兆全双工防止协商异常导致实际速率掉到百兆 sudo ethtool -K eth0 gso on gro on tso on # 开启TCP分段卸载对UDP收流没有帮助但对多路控制通道有间接帮助 sudo ip link set dev eth0 mtu 9000 # 开启巨型帧单个数据包可放下更多图像数据降低包率前两项是常规操作第三项要提醒一下巨型帧必须在相机和网卡两端同时开启。相机端的巨型帧一般通过配置工具改如果相机端没开你这边把MTU调到9000反而会产生大量分片丢包率会明显上升。最稳妥的做法是先把MTU设为1500跑通全链路再开启巨型帧做性能优化。防火墙的处置要特别提醒GVCP回复可能到达高位端口GVSP目标端口由相机动态分配所以放行规则不要只写死3956。Linux下可以临时把整个UDP入站放行调试Windows下调试期间把专用网络的防火墙先关闭跑通后再收窄规则。4.2 从发现到开始采集封装最小GVCP交互序列这一节直接给一个可以用Python复现的最小流程。逻辑顺序是发现设备读取XML描述长度获取流通道的端口和包大小设置采集模式然后开始传输。import socket import struct import time def read_register(sock, cam_ip, reg_addr): # 组装ReadReg命令 cmd struct.pack(HHHHHI, 0x42, 0x0010, 2, 0, 4, reg_addr) sock.sendto(cmd, (cam_ip, 3956)) data, _ sock.recvfrom(512) magic, ack, status struct.unpack(HHH, data[:6]) if status ! 0: raise RuntimeError(fReadReg失败错误码: 0x{status:04X}) return struct.unpack(I, data[8:12])[0] def write_register(sock, cam_ip, reg_addr, value): cmd struct.pack(HHHHHI, 0x42, 0x0011, 3, 0, 4, reg_addr) cmd struct.pack(I, value) sock.sendto(cmd, (cam_ip, 3956)) data, _ sock.recvfrom(512) magic, ack, status struct.unpack(HHH, data[:6]) if status ! 0: raise RuntimeError(fWriteReg失败错误码: 0x{status:04X})这里的关键参数有三处。0x0010是ReadReg的命令码0x0011是WriteReg的命令码这两个值在GigE Vision协议里是标准定义所有厂商相机通用。寄存器地址不通用不同相机的曝光寄存器、增益寄存器地址差异很大必须从设备描述文件里读取。第三个细节是回复包前6个字节的解析前2字节是魔数第3、4字节是ack id第5、6字节才是状态码。如果擅自把状态码位置往前挪两字节会发现所有操作永远返回成功。4.3 抓流通道重组UDP包并按block_id输出帧开始采集后程序进入一个循环收包、判类型、按block_id分组、等一帧齐了再回调出去。下面是最关键的收流循环import socket import queue import threading frame_queue queue.Queue(maxsize8) def stream_loop(ip, port): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((, port)) sock.settimeout(1.0) frames {} # block_id - {packet_count, payload} while True: try: data, _ sock.recvfrom(65535) except socket.timeout: continue # 解析GVSP头前8字节 block_id struct.unpack(Q, data[:8])[0] packet_type (data[10] 0xF0) 4 payload data[8:] # 去除GVSP头后的图像数据 if packet_type 1: # 帧起始包 (LEADER) frames[block_id] {data: bytearray(), count: 1} elif packet_type 2: # 帧中间包 (PAYLOAD) if block_id in frames: frames[block_id][data].extend(payload) frames[block_id][count] 1 elif packet_type 3: # 帧结束包 (TRAILER) if block_id in frames: frames[block_id][data].extend(payload) frame bytes(frames.pop(block_id)[data]) try: frame_queue.put_nowait(frame) except queue.Full: pass # 丢新保旧策略消费者太慢删最老暴露时先保最新帧GVSP头的解析容易出错。前8字节确实是64位的block_id但第8到第12字节的格式在不同长度包模式下有差异。上面代码里data[10]取的是包类型字段的高4位这是标准的包类型标志。实际调试时可以先用一个已知图像大小的设置跑一次然后判断收到的帧体字节数是否等于宽×高×位深。如果少了检查是不是没把帧尾包含进去如果多了大概率是把相机厂商自定义的扩展区块也当成图像数据了。流通道里的端口要特别注意port不是自己填一个固定值而是从控制通道读取相机当前流通道配置后得到的实际端口。在4.2的例子中读这个值通常要走一个READREG获取StreamChannel0Port地址对应的内容。如果跳过这一步直接拿3956去收图像数据程序会一直卡在recvfrom上而控制通道上却能正常通信。4.4 帧完整性判定用block_id连续性和分包数判断是否丢包拿到帧之后不要急着传给图像处理管线先做一次帧完整性校验。常见做法是维护一个计数器检查当前帧的block_id相对于上一帧是否连续递增1。不过这个判断在有些相机的多通道输出模式下会失效多通道时同一帧图像会被拆成多个block_id段分别输出每个段都复用同样的递增规则。更可靠的指标是每个block_id内的数据包总数。以一张640×480、8位灰度、每包1440字节的图像为例单帧总字节数是307200字节分包后大约是213个数据包首尾包内还有少量头部信息。如果收的数不足一定要把缺包率记到日志里。下面这段可以加在采集循环里expected_bytes width * height * bytes_per_pixel if len(frame) expected_bytes: # 记录缺包帧而不是直接丢弃便于事后分析网络环境 missing_rate.append((expected_bytes - len(frame)) / expected_bytes)缺包分两种一种是大面积丢包说明网卡环形缓冲溢出或者防火墙拦截这类问题主要靠调大网卡buffer和检查丢包日志解决另一种是单帧内缺少量包这在网络繁忙时常见这种缺包即使补包成功图像上也可能出现横向条纹状花屏视觉检测里会带来致命误判。如果源码包里没有把缺包帧与正常帧分开标记在应用侧一定要做一层封装区分。5. GigE Vision源码落地最容易踩的6个坑现象、原因与处置5.1 能发现相机但打不开相机现象设备枚举列表里有相机返回了完整的相机名和IP但执行打开设备接口时一直超时或报CONTROL_CHANNEL_BUSY。原因相机控制通道被其他进程占用。GigE Vision规定同一时刻控制通道的写权限只能归一个客户端所有源码包如果没实现CONTROL_CHANNEL_PRIVILEGE抢占逻辑就会一直拿不到权限。解决检查是否有前一个测试程序没有正常退出用任务管理器结束残留进程后重试。如果确认没有残留进程就在源码里加上权限抢占命令在每次打开设备时先发送设备访问权限请求等收到成功回复后再进行寄存器读写。需要注意的是抢占可能让另一个正在采集的客户端瞬间断流所以在多人同时调试同一台相机时不要默认开启抢占。5.2 收到第一帧图像后整帧是花屏现象图像能输出但内容像被横向撕开画面里有大量水平方向错位的色块或条纹。原因相机像素格式是Bayer某一种排列或YUV422交错而应用层按RGB888直接解析了。这不是协议丢包问题是像素格式不匹配。解决回到XML描述文件读取相机的当前像素格式节点确认值是BayerRG8还是Mono8再按对应格式转换。源码包里如果转换逻辑是硬编码的就要改成一个根据节点值动态分发的转换器。这个坑在换相机型号时特别容易重新出现因为不同厂商对同一色彩排列的命名也可能不同。5.3 能收控制回复但收不到任何图像数据现象打开相机、设置参数、开始采集都返回成功但流socket的recvfrom一直超时。原因开始采集命令执行了但相机没有收到有效的流通道目标地址或者相机正在等待图像触发信号。很多相机的触发模式默认不是内部自由运行而是外部触发或者软触发需要显式把触发源设置为内部。解决读取并设置TriggerSource节点为Internal或Software然后再次发送采集开始命令。另外检查流通道设置的destination_ip和destination_port是不是真的写进了相机的流通道寄存器。不排查寄存器值的话这个问题能折磨一整天。5.4 帧率只有标称的一半现象相机标称120fps实际采集只有60fps左右且CPU占用不高。原因网卡中断没有得到均匀分配或者巨型帧没有开启导致每包装载的数据量太小中断频率暴涨。也可能有多种原因相加网卡环形缓冲太小、CPU调频策略太保守、图像大小刚好超过巨型帧支持范围导致分片。解决先用ethtool -L查一下网卡队列数再在源码启动时把流线程绑定到固定CPU核心。巨型帧方面先确认相机端看板配置里MTU确实为9000然后看实测收包数是否明显下降。这里有个血泪经验开启巨型帧后首包反而丢得更厉害通常是因为网卡驱动里的rx-usecs调节了中断合并时间要配合调低合并时长。5.5 连续运行一段时间后图像断流现象程序刚启动一切正常运行几十秒或几分钟后突然收不到帧重启程序又恢复正常。原因网络栈自动协商导致网卡速率重新协商到百兆或相机的流通道与客户端失去了同步也可能网卡环形缓冲区被占满后驱动直接丢弃后续包。源码里的超时重连逻辑没覆盖这种情况就表现为断流后永久卡死。解决在采集循环里检查连续超时的次数比如连续3秒没收到任何包就触发重连逻辑。重连前先发送流停止命令再重新执行一次开始采集。不要直接关闭socket后立刻re-bind要等相机侧资源释放以秒为单位做延时。5.6 时间戳跳变且无法用于多相机同步现象采集模块能取到时间戳但时间戳在不同帧之间来回跳动多相机场景里相关性完全对不上。原因时间戳来源可能是相机内部时钟也可能来自主机接收时刻。源码包里如果直接取系统时间作为帧时间戳不同相机的数据就没有统一基准。解决多相机严谨方案是开启IEEE 1588 PTP同步源码包要保留专门的PTP线程维护时钟同步状态。没有PTP时不要承诺帧间精度只在日志里输出相机自带的timestamp字段作为参考。在测试阶段主程序只用时间戳做排序不要做严格差值运算。6. 验证采集稳定性的三个工具级技巧抓包、队列观测与CPU绑定验证GigE Vision源码的最终标准不是能不能出图而是长时间运行下丢包率是否在可控范围、CPU占用是否稳定。我给三个自用的验证技巧。第一个技巧是启动前先抓一次UDP包。在未运行采集程序时单独跑一个tcpdump命令抓相机发往本机的GVSP包看包头的block_id是否连续、每包大小是否符合预期。这样能排除代码因素先确认网络链路本身没问题。抓包只做小样本验证就行长时间抓包会放大丢包率干扰判断。第二个技巧是在源码层加入带时间戳的帧到达日志但日志不要每帧都写而是每100帧写一条。记录三组数值收到的block_id、该帧内分包数、从首包到尾包的时间差。观察这几组数据的变化趋势如果首尾包时间差在缓慢增大说明接收线程处理不过来堆积正在发生。第三个技巧是CPU绑定。把流接收线程绑定到与网卡中断相同的物理核心上能显著降低调度抖动。Linux下先用irqbalance --exclude排除网卡中断的自动迁移再在程序里用pthread_setaffinity_np绑定接收线程。绑定后观察收帧耗时方差通常会明显下降。我自己做相机接入有个习惯无论用哪个源码包第一件事永远是抓一把原始UDP包看真实报文而不是直接编译代码去连相机。报文结构和网上流传的示例差异往往就在一两个字节上光看源码猜协议排查一个多字节造成的错位可能耗掉大半天。希望这篇拆解能帮你在同样的路上少走几步尤其是那个收不到流的夜晚先看抓包再看日志不要闷头改代码。本文还有配套的精品资源点击获取