尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

基于Qt+OpenCV+海康相机SDK的工业视觉检测上位机开发实战

基于Qt+OpenCV+海康相机SDK的工业视觉检测上位机开发实战
📅 发布时间:2026/8/2 9:12:07

1. 项目缘起与核心价值

最近在做一个工业视觉检测的项目,客户现场有一套海康的工业相机,需要实时抓取产品图像,进行一些基础的尺寸测量和缺陷识别,然后把结果(比如OK/NG,或者测量值)发送给产线上的汇川PLC,由PLC来控制后续的机械臂分拣或者打标动作。这个需求在自动化产线上非常典型,但市面上现成的视觉软件要么太贵,要么不够灵活,没法深度定制通讯协议和界面逻辑。所以,我们决定自己动手,用Qt+OpenCV+海康相机SDK攒一个轻量级的图像处理上位机Demo。

这个Demo的核心价值在于,它打通了从图像采集、处理到工业控制指令下发的完整链路。Qt负责提供一个稳定、美观且跨平台的用户界面;OpenCV作为图像处理的“瑞士军刀”,处理从预处理到特征提取的所有算法任务;海康相机SDK则提供了稳定、高效的图像采集接口;最后,通过TCP Socket与汇川PLC通讯,将视觉系统的“眼睛”看到的结果,转化为控制系统的“手”可以执行的命令。对于自动化工程师或者视觉应用开发者来说,掌握这套技术栈,就意味着你能够独立搭建一个定制化的、可嵌入到更大MES或SCADA系统中的视觉检测单元,无论是做原型验证还是小批量部署,都极具性价比和灵活性。

2. 技术栈选型与项目架构设计

为什么是Qt+OpenCV+海康SDK+TCP Socket这个组合?这背后是经过一番权衡的。首先,Qt的跨平台特性(Windows/Linux)对于工业现场部署非常友好,其强大的信号槽机制和丰富的UI控件库,能让我们快速构建出带实时图像显示、参数配置、日志输出和状态监控的桌面应用。OpenCV的选择更是毋庸置疑,其庞大的社区和成熟的图像处理算法库,从简单的二值化、边缘检测到复杂的模板匹配、轮廓分析,几乎涵盖了视觉检测的所有基础需求,而且C++接口与Qt能无缝集成,性能有保障。

海康相机SDK(官方称为MVS,Machine Vision Suite)是连接硬件的桥梁。虽然OpenCV有通用的VideoCapture类,但对于工业相机,特别是需要精确控制曝光、触发、增益等参数,以及获取更高帧率和图像质量时,原厂SDK是必须的。它提供了更低延迟、更稳定的数据流和更丰富的相机控制功能。最后,与汇川PLC的通讯,我们选择了最通用、也最被PLC支持的TCP Socket协议。汇川的H5U、AM400/600等系列PLC都支持TCP Server或Client模式,通过发送约定的字符串或二进制指令,就能实现数据交换,避免了去集成复杂的、各家不一的上位机通讯库(如OPC UA、Modbus TCP库),让整个Demo更轻量、更聚焦于视觉逻辑本身。

整个项目的架构可以清晰地分为四层:

  1. 交互层(UI):基于Qt Widgets或QML构建的主界面,负责参数设置、图像显示、结果展示和手动控制。
  2. 采集层:封装海康SDK的相机操作,包括枚举设备、连接、参数设置、注册回调函数接收图像数据。
  3. 处理层:核心逻辑所在。接收原始图像数据(通常是cv::Mat格式),调用OpenCV函数进行一系列处理,最终输出检测结果和数值。
  4. 通讯层:一个独立的TCP客户端模块,负责与指定IP和端口的汇川PLC建立连接,并按照双方约定的数据格式(例如,“RESULT,OK\n”或“MEASURE,12.345\n”)定时或事件触发式地发送结果。

这四层之间通过Qt的信号槽进行松耦合通信。例如,采集层收到一帧图像后,发射一个携带cv::Mat的信号;处理层连接到这个信号,开始处理,处理完成后发射另一个携带结果的信号;UI层和通讯层都可以连接到结果信号,分别用于界面更新和网络发送。

3. 开发环境搭建与核心依赖配置

工欲善其事,必先利其器。搭建一个稳定且高效的开发环境是第一步,这里面的坑不少。

3.1 Qt安装与项目创建

建议使用Qt的在线安装器,选择最新的LTS版本(如Qt 5.15.2或Qt 6.2+)。组件选择上,必须勾选对应你编译器版本的Desktop Qt套件(例如MSVC2019 64-bit)和Qt Creator。对于这个项目,Qt Charts模块可能用于绘制测量数据曲线,可以一并安装。

在Qt Creator中新建一个Qt Widgets Application项目。在项目配置文件(.pro)中,我们需要提前规划好依赖。一个基础的.pro文件配置可能如下:

QT += core gui network concurrent greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++17 # 假设你的OpenCV和海康SDK都采用动态链接 # OpenCV路径 - 需要根据你的实际安装位置修改 win32 { INCLUDEPATH += D:/opencv/build/include LIBS += -LD:/opencv/build/x64/vc15/lib \ -lopencv_world455 } linux { INCLUDEPATH += /usr/local/include/opencv4 LIBS += -lopencv_core -lopencv_highgui -lopencv_imgproc # ... 按需添加其他模块 } # 海康威视MVS SDK路径 - 需要根据你的实际安装位置修改 win32 { INCLUDEPATH += D:/MVS/SDK/Include LIBS += -LD:/MVS/SDK/Library/Win64 \ -lMvCameraControl }

注意:OpenCV的库文件名(如opencv_world455)和路径必须与你安装的版本完全一致。海康SDK的库名和路径也需参照其开发文档。Linux下通常使用pkg-config来管理OpenCV会更方便。

3.2 OpenCV的编译与集成

虽然可以从官网下载预编译的Windows版本,但我强烈推荐自己从源码编译。原因有两个:一是可以定制化模块,减少最终发布程序的体积;二是能确保编译器和Qt使用的运行时库一致,避免诡异的运行时错误。

编译OpenCV时,使用CMake-GUI工具。关键配置:

  • CMAKE_INSTALL_PREFIX: 设置为你希望的安装路径,如D:/opencv_build。
  • BUILD_opencv_world: 如果勾选,会将所有模块打包成一个大的opencv_world4xx.dll,链接时更方便,但文件较大。不勾选则会生成许多独立的小库。
  • WITH_QT:务必勾选。这会让OpenCV的高层GUI模块(如imshow)使用Qt作为后端,避免与你的Qt应用程序产生窗口系统冲突。
  • 指定你的编译器为与Qt相同的MSVC版本。

生成解决方案后,在Visual Studio中打开,分别编译ALL_BUILD的Release和Debug版本,然后编译INSTALL项目。完成后,在安装路径下你会得到包含头文件、库文件和二进制DLL的完整结构,这就是上面.pro文件中需要配置的路径。

3.3 海康相机SDK的获取与注意事项

从海康威视机器人官网下载“机器视觉软件套装(MVS)”。安装后,在安装目录的SDK文件夹下找到Include和Library。这里有一个关键点:海康SDK依赖于其自身的运行时库。在开发时,除了链接MvCameraControl.lib,在部署程序时,必须将Library/Win64下的所有DLL(如MvCameraControl.dll,MvGIGEVision.dll等)拷贝到你的可执行文件同级目录。否则,程序在调用SDK函数时会崩溃,提示找不到入口点。

4. 海康相机SDK的封装与图像采集实现

直接在海康SDK的C风格API上裸写业务逻辑会很混乱,我们需要一个CameraController类来封装它。

4.1 相机枚举与连接

首先,在CameraController的头文件中,包含海康SDK的头文件MvCameraControl.h,并定义一个相机句柄void* m_hCamera作为成员变量。

枚举相机设备通常使用MV_CC_EnumDevices函数。这里我更喜欢使用网口(GigE)相机,因为其IP地址固定,比USB相机更稳定。封装一个QStringList CameraController::enumDevices()函数,将枚举到的设备信息(如IP地址、型号、序列号)添加到字符串列表中,供UI层的下拉框使用。

连接相机时,使用MV_CC_CreateHandle和MV_CC_OpenDevice。这里有一个经验:在调用MV_CC_OpenDevice后,最好立即设置一些关键参数,比如将相机采集模式设置为连续采集(MV_CC_SetEnumValue(“AcquisitionMode”, MV_ACQ_MODE_CONTINUOUS)),并关闭相机自带的自动曝光和增益(MV_CC_SetBoolValue(“ExposureAuto”, false)),以便后续由我们的程序精确控制。

4.2 图像数据回调与Qt集成

海康SDK推荐使用回调函数的方式获取图像数据,效率高于主动查询。我们需要注册一个回调函数:

// 静态成员函数或全局函数作为回调 void __stdcall ImageCallback(unsigned char * pData, MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser) { if (pFrameInfo) { CameraController* pController = (CameraController*)pUser; QMetaObject::invokeMethod(pController, "onImageDataReceived", Qt::QueuedConnection, Q_ARG(unsigned char*, pData), Q_ARG(MV_FRAME_OUT_INFO_EX*, pFrameInfo)); } } // 在CameraController中注册 MV_CC_RegisterImageCallBackEx(m_hCamera, ImageCallback, this);

注意,这个回调函数运行在海康SDK内部的采集线程中,不能直接在此进行UI操作或耗时的图像处理。我们通过QMetaObject::invokeMethod,以Qt::QueuedConnection方式将数据和帧信息抛给主线程的一个槽函数onImageDataReceived。这是Qt多线程编程的经典模式,确保线程安全。

在onImageDataReceived槽函数中,我们可以根据pFrameInfo中的信息(如像素格式、宽度、高度)将pData转换为OpenCV的cv::Mat。海康相机常用的像素格式是Mono8(灰度)或BayerRG8(需要插值转为彩色)。转换后,发射一个自定义信号,例如sigImageCaptured(cv::Mat),让处理模块去订阅。

4.3 相机参数控制

曝光时间、增益、帧率等参数的控制通过MV_CC_SetFloatValue等函数实现。为了UI的响应性,这些控制函数也应在主线程(或一个专用的控制线程)中调用。我们可以将这些调用封装成CameraController的公有槽函数,例如setExposureTime(int us),这样UI上的滑块或输入框的值改变信号,就可以直接连接到这些槽函数上。

5. 基于OpenCV的图像处理流水线设计

图像处理是上位机的“大脑”。我们需要设计一个可配置、可扩展的处理流水线。

5.1 图像预处理

采集到的原始图像通常包含噪声,且光照可能不均匀。预处理是保证后续处理稳定性的关键。

  • 灰度化:如果处理不需要颜色信息,第一步就是cvtColor(src, gray, COLOR_BGR2GRAY),能大幅减少计算量。
  • 滤波去噪:高斯滤波(GaussianBlur)或中值滤波(medianBlur)可以有效平滑噪声。对于椒盐噪声多的场景,中值滤波效果更好。
  • 光照补偿:如果现场光照不稳定,可以使用直方图均衡化(equalizeHist)或更先进的CLAHE(限制对比度自适应直方图均衡化)来增强对比度。
  • 二值化:这是将目标与背景分离的关键步骤。根据图像特点选择全局阈值threshold或自适应阈值adaptiveThreshold。一个重要的技巧:不要硬编码阈值,而是通过UI提供一个实时的阈值滑块,在调试阶段手动调整到最佳值,并记录这个值作为初始参数。甚至可以实现一个简单的“自动阈值”按钮,其背后调用threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU)使用大津法自动计算。

5.2 特征提取与测量

预处理后,我们得到的是一个二值图像,接下来是寻找我们关心的特征。

  • 轮廓查找:使用findContours函数。这里要注意RETR_EXTERNAL和RETR_TREE等检索模式的区别,以及CHAIN_APPROX_SIMPLE(压缩水平、垂直、对角线方向,只保留端点)对减少数据量的好处。对于简单的矩形物体,我们通常只关心最外层轮廓。
  • 轮廓筛选:不是所有找到的轮廓都是目标。我们需要根据面积(contourArea)、外接矩形大小、圆形度等几何特征进行筛选。例如,只保留面积在某个区间内的轮廓。
  • 几何测量:对于筛选后的轮廓,可以求取其最小外接矩形(minAreaRect)或拟合椭圆(fitEllipse),从而得到中心坐标、长宽、角度等信息。对于尺寸测量,关键是要有像素当量(每个像素代表的实际物理尺寸)。这需要通过标定来获取:在视野中放置一个已知尺寸的标定块(如10mm的方块),测量其在图像中的像素宽度,像素当量 = 实际尺寸 / 像素尺寸。后续所有像素测量值乘以这个当量,就得到了实际物理尺寸。

5.3 结果判定与数据封装

测量完成后,需要根据业务逻辑进行判定。例如,测量宽度是否在公差范围内,圆孔的中心距是否符合要求。判定结果(OK/NG)和具体的测量数值需要被封装成一个结构体或类,例如DetectionResult,里面包含状态、测量值数组、错误信息等。这个结果对象将作为信号参数,发送给UI和通讯模块。

6. TCP通讯模块与汇川PLC数据交互实战

这是连接虚拟世界(图像数据)和物理世界(PLC控制)的桥梁。

6.1 Qt网络模块与PLC角色设定

Qt提供了QTcpSocket类用于TCP客户端。在我们的架构中,上位机作为TCP客户端,汇川PLC作为TCP服务器是更常见的模式。因为PLC的IP地址通常固定,上位机主动去连接它。我们在汇川的编程软件(如AutoShop或InoProShop)中,需要配置PLC的以太网模块,开启TCP Server功能,监听一个特定的端口,例如502(注意,这不是Modbus端口,只是自定义的一个端口)。

6.2 通讯协议设计

这是最容易出问题的地方。TCP是流式协议,没有消息边界。我们必须自己设计一个简单的应用层协议,确保PLC能完整、正确地解析我们发送的每一条指令。 一个简单可靠的方案是:“指令头+数据内容+分隔符”。 例如:

  • 发送检测结果:“RESULT,OK\n”或“RESULT,NG,PIN_MISSING\n”
  • 发送测量值:“DATA, WIDTH, 12.345, HEIGHT, 5.678\n”

这里,,用于分隔数据字段,\n(换行符)作为消息结束符。在PLC端,我们可以用它的字符串处理指令,持续接收数据,并查找\n来分割出完整的一条指令,然后再解析逗号分隔的字段。

6.3 Qt中的实现与断线重连

在PLCCommunicator类中,核心成员是一个QTcpSocket对象。

void PLCCommunicator::connectToPLC(const QString &ip, quint16 port) { m_tcpSocket->abort(); // 断开已有连接 m_tcpSocket->connectToHost(ip, port); // 连接信号:connected(), disconnected(), errorOccurred(), readyRead() }

readyRead()信号触发时,表示有数据从PLC传来(可能是应答)。虽然我们这个Demo主要是上位机发数据,但良好的习惯是处理接收,用于接收PLC的确认或状态反馈。

发送数据时:

void PLCCommunicator::sendResult(const DetectionResult &result) { if (m_tcpSocket->state() != QAbstractSocket::ConnectedState) { qWarning() << "PLC not connected, cannot send result."; // 可以触发重连逻辑 return; } QString command = formatCommand(result); // 根据协议格式化字符串 QByteArray data = command.toUtf8(); // 转为UTF-8字节流 qint64 bytesWritten = m_tcpSocket->write(data); if (bytesWritten == -1) { qCritical() << "Failed to write data to socket:" << m_tcpSocket->errorString(); // 处理错误,可能也需要重连 } }

必须实现的特性是断线自动重连。监听disconnected()和errorOccurred()信号,在一个定时器中尝试重新连接。重连间隔最好采用指数退避策略,避免网络恢复瞬间的冲击。

6.4 汇川PLC端的程序要点

在汇川PLC的梯形图或结构化文本中,你需要:

  1. 使用TCP_Listen或类似功能块创建服务器。
  2. 使用TCP_Recv功能块接收数据。由于TCP是流,你需要一个缓冲区数组,持续将接收到的字节存入,并编写一段程序逻辑来在这个缓冲区中查找\n(0x0A)字符。找到后,将之前的所有字节作为一个完整报文取出处理,并将缓冲区中该报文之后的数据前移。
  3. 解析报文字符串,根据指令头(如“RESULT”)跳转到不同的处理逻辑,将数据部分(如“OK”)赋值给对应的PLC内部寄存器(如D100)或标志位(如M100)。
  4. 后续的机械手控制程序,就可以读取这些寄存器或标志位来执行相应动作。

7. 界面设计、多线程与性能优化

7.1 Qt界面布局与实时显示

主界面可以使用QWidget配合布局管理器。核心区域包括:

  • 图像显示区:使用QLabel来显示图像。我们需要频繁地将cv::Mat转换为QImage,再转换为QPixmap,最后设置给QLabel。这个转换过程要放在图像处理线程,避免阻塞UI。转换时注意图像格式(BGR vs RGB)和内存是否连续。
  • 参数控制区:使用QSlider、QDoubleSpinBox、QComboBox等控件,用于调整图像处理的阈值、滤波核大小等。这些控件的valueChanged信号连接到处理模块的相应槽函数,实现参数实时更新。为了流畅体验,对于滑块,可以连接sliderReleased()信号而不是valueChanged(),这样只在用户松开滑块时更新一次,避免拖动过程中连续触发大量处理。
  • 结果与日志区:使用QTableWidget或QTreeWidget显示测量结果,使用QPlainTextEdit或QListWidget作为日志输出窗口。

7.2 多线程架构避免界面卡顿

图像采集和处理都是计算密集型或I/O密集型任务,绝不能放在主线程(UI线程)。我们使用Qt的QThread或更高级的QtConcurrent框架。

  • 方案一(推荐):将CameraController和ImageProcessor对象移到独立的QThread中。主线程只负责UI更新和用户交互。它们之间通过信号槽通信,由于信号槽是线程安全的,数据(cv::Mat,DetectionResult)会自动在线程间传递。注意,传递cv::Mat时,要使用cv::Mat的引用计数机制,或者显式地深拷贝(clone()),避免多线程同时读写同一块内存。
  • 方案二:使用QtConcurrent::run来异步运行处理函数。这对于单次触发或非连续的处理任务更合适。对于连续的视频流处理,还是方案一的专用线程更稳定。

7.3 性能优化技巧

  • 图像缩放显示:相机分辨率可能很高(如500万像素),但UI显示区域有限。不要在UI线程中进行高分辨率图像的原尺寸转换和缩放,这很耗CPU。可以在处理线程中,先将处理结果图(如画上了轮廓和标注的图)缩放到一个适合显示的大小(例如800x600),再进行Mat到QPixmap的转换,然后发送给UI线程显示。
  • 处理流水线优化:不是每一帧都需要走完完整的处理流水线。可以设置一个处理间隔,或者由外部触发(如PLC的拍照信号)来控制处理频率。在ImageProcessor的槽函数中,可以加一个标志位或计时器,限制处理频率。
  • 内存管理:确保在回调函数、信号槽传递中,没有内存泄漏。海康SDK的pData缓冲区在其回调函数结束后可能会被复用或释放,所以如果我们需要异步处理,必须在回调函数内将图像数据拷贝出来(例如用memcpy到另一个缓冲区或直接构造cv::Mat的深拷贝)。

8. 项目集成、调试与部署发布

8.1 模块集成与信号槽连接

在MainWindow的构造函数或初始化函数中,创建各个模块的实例,并连接信号槽。

// 创建实例 m_cameraController = new CameraController(this); m_imageProcessor = new ImageProcessor(this); m_plcCommunicator = new PLCCommunicator(this); // 将控制器和处理器移到子线程 QThread* camThread = new QThread(this); m_cameraController->moveToThread(camThread); camThread->start(); QThread* procThread = new QThread(this); m_imageProcessor->moveToThread(procThread); procThread->start(); // 连接信号槽 // 相机采集到图像 -> 处理器进行处理 connect(m_cameraController, &CameraController::sigImageCaptured, m_imageProcessor, &ImageProcessor::processImage); // 处理器处理完成 -> 更新UI显示 connect(m_imageProcessor, &ImageProcessor::sigResultReady, this, &MainWindow::updateResultDisplay); // 处理器处理完成 -> 发送结果给PLC connect(m_imageProcessor, &ImageProcessor::sigResultReady, m_plcCommunicator, &PLCCommunicator::sendResult);

8.2 调试技巧与常见问题

  • 海康SDK初始化失败:检查DLL是否放对位置,相机IP是否设置正确,防火墙是否阻止了端口。使用海康自带的MVS客户端软件先测试相机是否能正常连接和预览。
  • OpenCV错误:最常见的missing DLL错误。将OpenCV的bin目录(包含opencv_world455.dll等)添加到系统PATH,或者直接拷贝到你的可执行文件目录下。在Qt Creator的Projects -> Run设置中,也可以添加Environment变量。
  • TCP连接失败:首先用网络调试助手(如NetAssist)测试PLC的TCP Server端口是否确实在监听。检查IP地址、端口号、子网掩码和网关设置。确保PLC和工控机在同一个局域网段,且没有IP冲突。
  • 图像处理结果不稳定:光照!光照!光照!工业现场的光源稳定性是第一位的。考虑增加光源、使用漫射板、或采用更鲁棒的处理算法(如基于边缘的模板匹配代替简单的二值化)。

8.3 程序打包与部署

使用Qt自带的windeployqt工具可以自动拷贝大部分Qt依赖的DLL。但是,OpenCV和海康SDK的DLL需要手动拷贝。

  1. 在Qt Creator中以Release模式编译项目。
  2. 打开Qt命令行,切换到你的Release版可执行文件(.exe)所在目录。
  3. 执行windeployqt your_app.exe。
  4. 手动将OpenCV的DLL(如opencv_world455.dll)和海康SDK的DLL(MvCameraControl.dll等)拷贝到同一目录。
  5. 创建一个platforms文件夹,并将Qt安装目录下plugins/platforms里的qwindows.dll拷贝进去(windeployqt通常会做好这一步,但有时需要检查)。
  6. 最终,将这个包含.exe和所有DLL的文件夹打包,就可以部署到没有开发环境的工控机上了。

相关新闻

  • 2026年成都澳洲高尔夫定制旅行服务商口碑观察:诚信经营与专业能力解析 - 优质品牌商家
  • 【限时开源】我们刚交付的AI算力成本治理平台v2.3:自动识别低效Job、预测峰值负载、生成优化建议(仅开放72小时试用)
  • 2026 年 8 月成都非急救医疗转运产业全景调研与本土合规企业运营实录 - 平台推荐官

最新新闻

  • 1.69英寸SPI LCD驱动全解析:从硬件连接到GUI优化实战
  • 游戏开发中高可用Buff系统架构设计:从核心原理到Unity实践
  • 嵌入式TFT LCD API设计:从硬件抽象到图形界面构建实战
  • 魔兽争霸III性能优化终极指南:如何让经典游戏在现代电脑上流畅运行
  • 深度学习图像分类数据集—百种病虫害分类
  • 5分钟掌握ViGEmBus虚拟游戏手柄驱动完整指南

日新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

周新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号