ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VS+Qt实现工业相机实时取流与界面显示完整指南

VS+Qt实现工业相机实时取流与界面显示完整指南 工业相机界面开发这件事听起来好像不算多难但真正动手做的时候你会发现它卡人的地方根本不在界面而在图像数据到底怎么从相机里出来再转到Qt能认的格式上。这个链路里任何一个环节掉链子结果就是黑屏、花屏、程序崩溃或者界面卡成PPT。我写这篇demo系列的第一篇目标只有一个带你把相机取流界面显示这条主线完整跑通用Visual Studio加Qt搭出一个能实时显示工业相机画面的最小可用界面并顺手把图像保存功能做出来。适合刚接触工业相机开发、准备做设备上位机或视觉项目的朋友参考不需要你有很深的Qt基础但至少要把C基本语法捡起来。这个demo做出来之后你会得到三个东西一个能正常创建和销毁相机的界面框架、一个稳定出图的实时显示窗口、一套踩过坑之后沉淀下来的环境配置方案。我尽量把每一步为什么这样做讲透而不是简单丢给你一段能跑的代码。1. 先把目标拆清楚这套技术栈到底在解决什么问题工业相机界面开发严格来说是三件事的拼接相机SDK调用、图像数据格式转换、Qt界面渲染。很多人一上来就盯着Qt的控件研究结果相机连不上或者图像数据显示不出来折腾半天不知道问题出在哪。实际上这三件事有严格的先后依赖关系——SDK取不到数据后面全是空谈。1.1 相机、SDK、界面三者的角色分工工业相机负责采集原始图像数据输出的是裸数据raw data通常是Mono8灰度、BayerRG8彩色raw或者RGB8这种格式不经过任何解码处理。相机SDK厂商提供的一套动态库和头文件封装了相机的枚举、连接、参数配置、取流控制等操作。海康的MVS、Basler的pylon、大华的SDK接口风格大同小异。Qt界面负责把图像数据显示出来、接收用户的按钮操作、展示相机状态信息。Qt本身不关心你的图像是从相机来的还是从文件读的只要给它一个QImage或者QPixmap它就能往控件上画。1.2 为什么选择VS加Qt而不是其他组合这个组合在工业视觉领域是绝对的主流没有之一。工业相机的SDK基本都是C接口或C接口VS对C工程的管理、调试体验极其成熟而Qt负责UI层足够高效信号槽机制天然适合处理相机回调数据。这个demo不需要复杂的跨平台支持也不需要炫酷的界面特效要的是稳定、可控、好调试VS配Qt就是最稳的组合。1.3 Demo的目标范围本篇的demo聚焦在一条主线上枚举并连接一台工业相机连续采集图像并以实时画面的形式显示在Qt窗口上提供保存当前帧的功能断开相机并释放资源软件触发、硬触发、多相机、参数调节这些功能属于二和三的内容后面文章再展开。第一篇把这条最核心的链路打通了后面加功能只是在此基础上的增量工作。2. 环境准备VS与Qt的版本搭配比你想的更讲究这里我必须先把版本问题单独拿出来讲因为大部分人第一次搞Qt开发要么是在编译时报一堆莫名其妙的错误要么是插件装上了但VS里创建不了Qt项目归根结底都是版本匹配出了问题。2.1 版本匹配的黄金组合我用的是Visual Studio 2019v142工具集 Qt 5.14.2 msvc2017_64 Qt Visual Studio Tools插件这个组合。为什么这样选原因有三Qt 5.14.2是目前兼容性和稳定性最均衡的版本。5.15开始不提供离线安装包必须在线安装6.x的模块划分和编译方式变化较大很多老SDK示例代码还在用5.x的写法。msvc2017_64的Qt库可以完美配合VS2019编译编译器版本向后兼容这是官方支持的场景。VS2019对Qt的插件支持非常成熟调试和部署都很顺畅。这里有个容易忽略的细节你一定要搞清楚自己装的是哪个编译套件的Qt。如果你装的是mingw73_64版本的QtVS是没法直接用的因为VS的编译器是MSVC两者生成的二进制互不兼容。你需要装带msvc字样的版本。2.2 安装顺序和关键配置安装顺序推荐先装VS2019选C桌面开发工作负载再装Qt 5.14.2勾选MSVC 2017 64-bit模块最后装VS扩展插件。Qt安装完成之后需要手动添加C:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin到系统PATH环境变量中否则程序运行时找不到Qt5Core.dll这些动态库直接报由于找不到Qt5Core.dll无法继续执行代码。装完插件后在VS里打开扩展菜单找到Qt VS Tools选择Qt Versions添加你本机的Qt安装路径。这一步不做的话创建项目时根本没有Qt选项。注意如果插件装完了但VS菜单里没有Qt相关选项检查一下你装的VS版本和插件版本是否匹配。Qt VS Tools新版本只支持VS2019以后的部分版本老版本插件在VS2019上也能用但要在扩展里手工启用。2.3 一个容易踩的模块缺失问题如果你在自己的项目里往.pro文件加了QT serialport或者其他模块编译时报unknown module(s) in QT: serialport千万不要以为是自己代码写错了。这是典型的装Qt时没有勾选该模块导致的。回到Qt安装器里找到Qt 5.14.2勾选Additional Libraries把SerialPort、Charts这些需要的模块补上就可以了。我的demo不依赖这些扩展模块只用到了核心的QtWidgets和QtGui所以这一节算是一个预防性提醒。3. 项目结构设计从VS里创建一个能跑起来的Qt应用环境配好后打开VS在Qt VS Tools菜单下选择Create New Qt Widgets Application项目名称我建议直接叫CameraDemo。这里有几个选项需要注意一下。3.1 项目创建参数创建项目的时候会问你要哪些Qt模块最少勾选QtCore、QtGui、QtWidgets三个。如果你后面要用到网络或串口再加对应模块现在不需要。生成的工程里默认会有一个main.cpp和一个主窗口类我建议把主窗口类改名为CameraWindow语义更明确。主要文件结构如下// main.cpp #include QtWidgets/QApplication #include CameraWindow.h int main(int argc, char *argv[]) { QApplication a(argc, argv); CameraWindow w; w.show(); return a.exec(); }3.2 界面布局怎么排控件才算合理这个demo主窗口大概需要这些控件一个QLabel用来显示图像、一个QPushButton用来连接/断开相机、一个QPushButton用来保存当前帧、一个QLabel或QLineEdit用来显示相机连接状态和信息。界面布局上我用的是QGridLayout把图像显示区域放在左上角并拉伸填满右侧放一排操作按钮下方放一个状态栏。这个布局的好处是窗口任意缩放时图像区域会自动跟着变大按钮保持固定大小。// 在CameraWindow的构造函数中创建布局 QVBoxLayout* rightLayout new QVBoxLayout(); rightLayout-addWidget(btnConnect); rightLayout-addWidget(btnSave); rightLayout-addWidget(labelStatus); rightLayout-addStretch(); QHBoxLayout* mainLayout new QHBoxLayout(); mainLayout-addWidget(labelImageDisplay, 1); mainLayout-addLayout(rightLayout); centralWidget()-setLayout(mainLayout);QLabel左上角那行比较乱不是忘了加布局而是我特意没有给图像显示增加padding因为我希望显示区域越接近真实图像大小越好调试的时候不会因为多余的边框干扰判断。3.3 信号槽机制在demo中的用法Qt的信号槽是理解这套框架的关键。简单说就是某个事件发生的时候主动去调用另一个函数。在这个demo里点击连接相机按钮就会触发onConnectButtonClicked()函数这个连接不需要你手动写回调注册Qt已经帮你做好了。connect(btnConnect, QPushButton::clicked, this, CameraWindow::onConnectButtonClicked);这里有个细节值得注意connect的对象的生命周期。如果你用类成员做界面元素生命周期跟随主窗口连接就不会出问题。如果你临时new一个控件还不设置父对象点击时可能crash关于生命周期问题后面还会提到。4. 相机SDK接入从枚举到取流的完整链路这是整个demo的核心也是网上资料最零散的部分。工业相机品牌很多海康、Basler、大华、映美精各有各的SDK接口命名和调用顺序也不完全一样。但核心流程几乎都是固定的五步。我以海康机器人MVS SDK为例展开Basler和其他的SDK接口名字不同逻辑完全可对照。4.1 五步主流程// 1. 枚举相机并选择设备 MV_CC_DEVICE_INFO_LIST deviceList; memset(deviceList, 0, sizeof(deviceList)); MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, deviceList); // 2. 创建相机句柄 MV_CC_HANDLE handle NULL; MV_CC_CreateHandle(handle, deviceList.pDeviceInfo[0]); // 3. 打开相机 MV_CC_OpenDevice(handle); // 4. 设置参数并开始取流 MVCC_INTVALUE stParam; memset(stParam, 0, sizeof(MVCC_INTVALUE)); MV_CC_GetIntValue(handle, PayloadSize, stParam); MV_CC_StartGrabbing(handle); // 5. 取流结束停止并销毁句柄 MV_CC_StopGrabbing(handle); MV_CC_CloseDevice(handle); MV_CC_DestroyHandle(handle);很多新手不知道的是第2步到第3步之间还有一个隐藏的细节——MV_CC_CreateHandle之后需要判断是否成功。如果相机被别的软件占用了MV_CC_OpenDevice会返回错误码但如果你不检查返回值程序会继续往下走然后在一个无效的句柄上调用StartGrabbing可能直接崩溃也可能没有任何反应。所以完整代码里每一步都要检查返回值。工业软件开发的第一原则就是对所有SDK函数返回值做判断绝不允许先跑通再说。4.2 回调取流不要在UI线程里做耗时工作取流有两种模式主动调用MV_CC_GetOneFrameTimeout被动取一帧适合定时抓拍场景或者注册图像回调函数适合连续显示。我的demo用回调模式因为实时显示场景要求图像一到就立刻处理不做等待。MV_CC_RegisterImageCallBackEx(ImageCallBackEx, this);这个回调函数是SDK内部的工作线程调用的它运行在非UI线程。这里有一个很多新手必修课UI相关的操作不能在线程里直接执行Qt在这方面已经有成熟的机制——把图像数据通过信号传递到主线程再更新界面。4.3 回调中的数据处理与跨线程通知回调函数的典型结构void __stdcall ImageCallBackEx(unsigned char* pData, MV_FRAME_OUT_INFO_EX* pFrameInfo, void* pUser) { CameraWindow* pWindow (CameraWindow*)pUser; if (pWindow) { pWindow-onImageReceived(pData, pFrameInfo); } }在onImageReceived里我先把数据复制出来然后发射一个Qt信号让它在主线程里执行真正的图像转换和界面更新。这里的关键是Qt::QueuedConnection——当信号在非UI线程发射时槽函数会排队到UI线程的事件循环中执行这样就避免了直接在回调里操作界面导致的崩溃。// CameraWindow.h中定义 signals: void imageReady(QImage image); // CameraWindow.cpp中构造函数里连接 connect(this, CameraWindow::imageReady, this, CameraWindow::updateImageDisplay, Qt::QueuedConnection); // 回调触发的函数 void CameraWindow::onImageReceived(unsigned char* pData, MV_FRAME_OUT_INFO_EX* pFrameInfo) { // 这里可以根据pFrameInfo中的图像格式、宽高来构造图像 // 由于回调在非UI线程这里不能直接操作UI // 所以把数据封装成QImage后发信号 QImage image(...); emit imageReady(image); }4.4 工程配置头文件、库文件和dll一个都不能少在VS里配置SDK需要在项目属性里做三件事VC目录 - 包含目录添加SDK安装路径下的Development\IncludesVC目录 - 库目录添加SDK安装路径下的Development\Libraries\win64链接器 - 输入 - 附加依赖项添加MvCameraControl.lib这里有个细节库文件分32位和64位VS的项目平台要选成x64。如果你Debug模式下用MvCameraControl.libRelease模式下也用同一个lib但运行时链接的dll不同。海康SDK的lib文件和dll文件在64位下是通用的指的是Debug和Release能用同一个但32位程序必须用32位的库和dll。还有一个很容易漏掉的问题把MvCameraControl.dll放到最终exe同目录下或者放到系统PATH中的某个目录。我推荐的做法是在项目属性里的生成事件中添加一条拷贝命令每次编译完自动把dll复制到输出目录省得每次都手动拷贝。5. 图像数据转换从相机裸数据到QImage的账要算清楚相机SDK拿到的数据是一块内存缓冲它本身没有图片的概念只是一个字节流。你要把它显示到Qt的QLabel上必须先转换成QImage。这一步的难点在于不同的像素格式对应不同的内存组织方式转错了就是花屏。5.1 常见的三种像素格式Mono8每个像素1个字节范围0到255灰度图。这是黑白工业相机最常用的格式转换最简单。BayerRG8每个像素1个字节但有颜色信息需要用拜耳插值算法转成RGB。工业彩色相机输出默认格式转换后颜色才正常。RGB8每个像素3个字节分别代表红绿蓝分量。部分相机支持直接输出RGB8省去颜色插值步骤。5.2 用QImage直接包装还是走转换接口如果SDK直接输出RGB8在回调里直接用QImage(pData, width, height, QImage::Format_RGB888)包一层就行不需要做内存拷贝。但有个问题QImage是浅拷贝它引用的是你传入的内存指针。一旦你把这个指针传递给信号槽而回调函数返回后SDK又往这块内存里写了下一帧数据那么你界面里正在显示的图像就会被新数据覆盖——出现画面闪烁或者一半新一半旧的现象。所以在demo里我宁可在回调中做一次image.copy()深拷贝把数据真正复制出来也不要去冒险用浅引用。后续优化的时候可以用QSharedPointer加上缓冲池来减少拷贝开销但那属于进阶内容第一篇先保靠。如果是Mono8灰度图QImage直接支持的格式是Format_Grayscale8这个格式Qt 5.5之后才支持所以5.14.2没有问题直接QImage image(pData, width, height, QImage::Format_Grayscale8); QImage rgbImage image.convertToFormat(QImage::Format_RGB888);如果是BayerRG8那么老老实实用SDK提供的像素格式转换接口先转成RGB8再包成QImage。海康MVS里调用MV_CC_ConvertPixelTypeBasler的接口叫ConvertTo功能都一样。5.3 数据拷贝的高效姿势在回调里做copy()没问题但你要知道它内部是怎么工作的。QImage::copy()会分配一块新的内存然后把原来的像素数据逐行拷贝过去。在图像分辨率比较大比如500万像素、10帧每秒时这个开销是存在的但因为Qt底层用memcpy优化过比你自己逐像素复制快得多。真正不建议的做法是逐行逐像素去赋值那会把CPU耗在高延迟的内存读写上。如果实在担心拷贝开销可以考虑用QImage的扫描线scanLine()配合std::memcpy手动做行拷贝思路一样但灵活性更高。5.4 界面上为什么看到的是花屏花屏的原因九成是格式不匹配。灰度的数据硬用RGB格式去解释颜色就会错乱。如果你确认格式匹配了还花屏第二怀疑对象是stride步长不对。工业相机输出的每一行数据可能有对齐填充比如宽度1024像素、每个像素2字节那么每行实际的字节数可能不是2048而是2048加上几个补齐字节。QImage的构造函数里有一个bytesPerLine参数如果你明确知道步长就应该把它传进去否则QImage默认按width * 4推算和实际内存布局对不上显示出来的图像就是错位的。QImage image(pData, width, height, bytesPerLine, QImage::Format_Grayscale8);如果不确定步长就先打印bytesPerLine和width来对比。能走到这一步离成功出图就差最后一步了。6. 界面显示与图像保存从QImage到QLabel的最后一公里图像数据转成QImage之后界面显示反而成了最简单的事。但在实现保存当前帧功能时有一些细节值得你留意。6.1 QLabel显示图像的效率问题QLabel本身不是为高频刷新设计的。它继承自QFrame内部用QPixmap来显示图像。每秒刷新十几次甚至几十次直接用setPixmap是可以跑的但有一个效率陷阱每次传QPixmap时做了一次QPixmap和QImage的转换这个转换是CPU密集的。更高效的姿势是在UI线程里用QPixmap::fromImage显式转换后设置到QLabel上。另外显示区域最好保持和图像宽高比例一致否则QLabel会缩放图像每帧缩放也都消耗CPU。在这个demo里我用labelImageDisplay-setScaledContents(true)允许缩放但设置了setMinimumSize(640, 480)保证界面不会缩成一团。如果追求实时性能应该把显示区域固定为相机输出分辨率。6.2 保存当前帧的完整实现保存按钮的逻辑很简单把当前界面正在显示的QImage写到本地文件。但这里有坑——用户点击按钮的时机和图像更新的时机不一定是同步的。如果我在回调线程里更新一个成员变量m_currentImage主线程的按钮槽函数直接读它理论上不会有问题因为这是同一线程的读写但如果保存函数在按钮点击时访问的是一块正在被回调线程拷贝的内存就存在数据竞争。稳妥的做法是把当前帧保存在一个加了锁保护的成员中或者直接保存一份副本在按钮槽函数里。我的方案是保存一份副本void CameraWindow::onSaveButtonClicked() { if (m_currentImage.isNull()) { QMessageBox::warning(this, 提示, 当前没有图像); return; } QString filename QFileDialog::getSaveFileName(this, 保存图像, capture.png, Images (*.png *.jpg *.bmp)); if (!filename.isEmpty()) { bool ok m_currentImage.save(filename); if (ok) { labelStatus-setText(图像已保存到: filename); } } }这里m_currentImage要确保在回调里每帧更新。保存用PNG格式是首选是无损压缩调试时用来分析图像细节不会损失质量。如果只是存图留档JPG会更省空间。6.3 图像尺寸适配界面如果你的相机分辨率极高比如1200万像素回调里的QImage会非常大显示到QLabel时如果直接做等比缩放缩放本身也要消耗资源。一个折中策略是界面显示用的图像单独维护一个缩小版本比如显示分辨率固定为800宽保存时用的则是原始分辨率版本。界面显示不追求像素级精度缩放了也不影响观感还能显著减少UI线程的负载。// 在UI更新时做缩放而不是在回调里缩放 void CameraWindow::updateImageDisplay(QImage image) { m_currentImage image; // 保存原始图 QImage scaled image.scaled(labelImageDisplay-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); labelImageDisplay-setPixmap(QPixmap::fromImage(scaled)); }注意scaled这一步尽量只在窗口尺寸变化时才重新计算否则每一帧都缩放一次参数调不好就会掉帧。改进方案是只在resizeEvent里重新缩放当前帧并更新界面。7. 实测结果与踩坑记录跑通demo时避不开的几个坎这部分是我最想写的内容。整个demo开发过程中我踩了少说得有七八个坑有些是环境配置有些是逻辑设计有些是SDK调用细枝末节每一个单独拿出来都够卡你半天。我把最典型的几个列出来你如果照着做能少走很多弯路。7.1 相机黑屏回调始终不触发现象是程序能跑起来点击连接相机之后没有任何报错但界面上什么也没有。排查下来发现是忘了启动取流只做了MV_CC_OpenDevice没有调用MV_CC_StartGrabbing。从SDK的设计逻辑看打开设备和开始取流是两个独立动作是为了让你能在正式采图之前做完所有参数配置。建议连接相机之后先把开始取流和停止取流做成独立接口界面上每次都显式调用不要偷懒。另一种黑屏是相机没被其他程序占用但GetOneFrameTimeout一直超时。这种多半是网口相机IP不对。你可以理解成网口相机和电脑之间的通信本质就是以太网通信设备的IP必须和电脑网卡在同一个网段。海康MVS的官方工具MVS Client里把相机IP改成和电脑一致就能通。命令行里ping通了相机的IP再回到自己的程序里连接一般就没问题了。7.2 DLL缺失导致的运行失败编译通过了一运行就弹窗说找不到MvCameraControl.dll或者Qt5Widgets.dll。这种问题不在于代码而在部署。开发阶段最省事的做法就是把dll一股脑放到exe所在目录。这里有个经验不要手动拷贝dll用构建脚本或者VS的生成事件把SDK的dll、Qt的bin目录C:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin里的相关dll都复制过去。你能稍微省心一点。如果要做发布用windeployqt工具把所有Qt依赖一次性部署到目标目录然后再手动拷相机SDK的dll。这个流程对新手来说最稳不会因为漏掉某个dll导致客户机器上跑不了。7.3 每秒钟几十帧的回调界面却卡成PPT这是新手优化路上遇到的第一个性能瓶颈。根本原因在于回调线程、图像转换、界面刷新全在同一时间做了太多事情。我的优化策略分三层第一层回调中只做复制数据和发射信号图像转换、缩放、界面更新全部放到UI线程。第二层UI线程中加定时器做压帧显示取流继续全速跑但界面每100毫秒才刷新一次10 fps刷新率这样视觉上流畅CPU负载还能降下来。第三层不用的中间变量及时清理拷贝出来的QImage用完立刻释放。// 用QTimer控制界面刷新频率 QTimer* displayTimer new QTimer(this); displayTimer-setInterval(100); connect(displayTimer, QTimer::timeout, this, CameraWindow::refreshDisplay); displayTimer-start();每帧都在刷新和每100毫秒刷新一次视觉上区别不大但CPU占用能下降一半以上。7.4 程序退出时崩溃很多人在退出程序时遇到崩溃问题几乎都出在析构顺序上。你想想如果主窗口已经被销毁了但SDK的回调线程还在往界面上发信号这些信号就没有对象来接收了。正确做法是在关闭窗口前先停掉取流、关闭相机、注销回调最后再销毁窗口。void CameraWindow::closeEvent(QCloseEvent* event) { // 先断开相机再销毁界面资源 onDisconnectCamera(); QWidget::closeEvent(event); }不加这一步程序在退出时会随机崩溃而且这个崩溃在Debug模式下才能看到具体信息Release模式下可能直接无响应。实际上这就是一个资源释放顺序的问题和之前说的DLL顺序逻辑一样——先释放依赖方再释放被依赖方。7.5 想用串口时真的没有串口模块这个坑我不止遇到一次。有的项目里图像显示部分已经跑通了客户要求加一个通过串口控制的逻辑代码里#include QtSerialPort/QSerialPort编译直接报No such file or directory检查.pro文件也写了QT serialport就是不行。原因是Qt安装时没有勾选SerialPort模块。回到Qt安装程序里选中Qt 5.14.2组件展开Add or remove components把SerialPort勾上然后重新编译。这个坑和前面unknown module in QT: serialport报错是完全相同的根因。7.6 中文字符串出现乱码在VS里写Qt代码如果源文件是UTF-8编码在Windows上编译经常会遇到中文字符串显示乱码的情况尤其当你的系统区域设置是简体中文时。建议所有源文件强制设为UTF-8 with BOM编码在VS的文件 - 高级保存选项里设置或者在每个源文件顶部加#pragma execution_character_set(utf-8)。Qt 5.14.2加了这个pragma之后中文字符串显示基本没问题。如果还是乱码检查一下你的QString赋值是否用了tr()或QStringLiteral做包装至少保证编译器按UTF-8处理。8. 下一步扩展思路这个demo还能往哪里长这个demo跑通之后的拓展方向其实非常多我根据自己的开发经验给你几条实际的路线参考。我这里不会做展望式总结就讲我下一步打算在这个demo基础上做什么以及为什么不继续做。首先我下一步打算做的是相机参数面板。曝光时间、增益、白平衡这些参数可以通过UI的滑动条和下拉框直接映射到SDK的MV_CC_SetFloatValue、MV_CC_SetEnumValue等接口上。这部分不涉及新的架构变化只是在现有主窗口上增加一组控件和响应的逻辑。做的时候要留意整数和浮点参数用不同的Set接口别搞混。其次考虑在采集的同时把图像写成一个视频文件。海康SDK提供录像接口但更通用的手段是把帧数据送到FFmpeg或者OpenCV的VideoWriter里编码。没有做这一步的原因是这个demo里如果边采集边编码CPU占用会立刻拉高从实时性角度考虑可能得不偿失所以单独抽出来作为一个离线处理的场景会更干净。如果你打算做视觉测量或者缺陷检测方向接下来就沿着在采集到的图像上叠加绘制结果这条路走。Qt的QPainter可以直接在QImage上画矩形框、十字线、文字等识别结果实时标注到画面上。这个功能视觉冲击力很强实现也不算难但前提是你已经能把图像稳定地显示出来——这也正是这个demo帮你打好的底子。至于工业相机丢帧、网速不对、未收到触发信号这些实际现场问题我后面会单独开篇从信号链路的角度讲。这一篇先到这里希望对正在搭第一个工业相机界面的你有实质性的帮助。
返回列表