当前位置:   article > 正文

Qt+OpenVino部署yolo5模型_qt openvino

qt openvino

一、openvino简介

OpenVINO是英特尔针对自家硬件平台开发的一套深度学习工具库,包含推断库,模型优化等等一系列与深度学习模型部署相关的功能。

OpenVINO™工具包是用于快速开发应用程序和解决方案的综合工具包,可解决各种任务,包括模拟人类视觉,自动语音识别,自然语言处理,推荐系统等。该工具包基于最新一代的人工神经网络,包括卷积神经网络(CNN),循环和基于注意力的网络,可在英特尔®硬件上扩展计算机视觉和非视觉工作负载,从而最大限度地提高性能。它通过从边缘到云的高性能,人工智能和深度学习推理来加速应用程序。

install:

https://nickhuang1996.blog.csdn.net/article/details/81385008

二、qt 中配置

安装好后,在pro文件中添入如下信息

  1. OPENVINO_ABS_PATH=$$quote(C:/Program Files (x86)/Intel/openvino_2021.4.752)
  2. OPENVINOPATH=$$OPENVINO_ABS_PATH/inference_engine
  3. OPENVINO_LIB=$$OPENVINOPATH/lib/intel64
  4. OPENVINO_INC=$$OPENVINOPATH/include
  5. INCLUDEPATH += $$OPENVINO_INC
  6. CONFIG(debug, debug|release) {
  7. LIBS += -L$$OPENVINO_LIB/Debug
  8. LIBS += -linference_engined \
  9. -linference_engine_c_apid \
  10. -linference_engine_transformationsd \
  11. } else{
  12. LIBS += -L$$OPENVINO_LIB/Release
  13. LIBS += -linference_engine \
  14. -linference_engine_c_api \
  15. -linference_engine_transformations \
  16. }
  17. OPENCVPATH=$$OPENVINO_ABS_PATH/opencv
  18. OPENCV_LIB=$$OPENCVPATH/lib
  19. OPENCV_INC=$$OPENCVPATH/include
  20. INCLUDEPATH += $$OPENCV_INC
  21. CONFIG(debug, debug|release) {
  22. LIBS += -L$$OPENCV_LIB
  23. LIBS += -lopencv_core453d \
  24. -lopencv_dnn453d \
  25. -lopencv_ml453d \
  26. -lopencv_highgui453d \
  27. -lopencv_imgcodecs453d \
  28. -lopencv_imgproc453d \
  29. -lopencv_video453d
  30. }else{
  31. LIBS += -L$$OPENCV_LIB
  32. LIBS += -lopencv_core453 \
  33. -lopencv_dnn453 \
  34. -lopencv_ml453 \
  35. -lopencv_highgui453 \
  36. -lopencv_imgcodecs453 \
  37. -lopencv_imgproc453 \
  38. -lopencv_video453 \
  39. -lopencv_videoio453
  40. }

三、yolo5部署代码

  1. #ifndef YOLO5PLUGIN_H
  2. #define YOLO5PLUGIN_H
  3. #include "DefectInterface.h"
  4. #include <inference_engine.hpp>
  5. #include <opencv2/dnn/dnn.hpp>
  6. #include <opencv2/opencv.hpp>
  7. using namespace InferenceEngine;
  8. using namespace cv;
  9. class QIcon;
  10. struct T_Yolov5sYamlData
  11. {
  12. QVector<QString> names;
  13. QVector<bool> shows;
  14. };
  15. class Yolo5Plugin : public QObject, public DefectInterface
  16. {
  17. /* 注意: QObject只能放在这里,放在父类继承会编译错误 */
  18. Q_OBJECT
  19. #if QT_VERSION >= 0x050000
  20. Q_PLUGIN_METADATA(IID IPlugin_iid FILE "Yolo5Plugin.json")
  21. #endif // QT_VERSION >= 0x050000
  22. Q_INTERFACES(DefectInterface)
  23. public:
  24. typedef struct
  25. {
  26. float prob;
  27. int label;
  28. QRectF rect;
  29. } T_Object;
  30. Yolo5Plugin();
  31. ~Yolo5Plugin();
  32. PluginSpec::E_TypeFlag typeFlag() const;
  33. QIcon icon() const;
  34. int init(const T_DefectSpec &spec);
  35. bool isInitialized() const;
  36. int run(const T_DefectSpec &spec, T_DefectResult &result);
  37. DefectInterface *newInstance();
  38. private:
  39. double sigmoid(double x);
  40. std::vector<int> get_anchors(int net_grid);
  41. bool parse_yolov5(const Blob::Ptr &blob, int net_grid,
  42. float cof_threshold, int item_size,
  43. std::vector<cv::Rect> &o_rect,
  44. std::vector<float> &o_rect_cof,
  45. std::vector<float> &o_labels);
  46. void resize(const cv::Mat &graySrc, cv::Mat &dst, double &scale);
  47. bool processFrame(const cv::Mat &gray, int item_size, std::vector<T_Object> &detected_objects);
  48. private:
  49. std::string _input_name;
  50. ExecutableNetwork _network;
  51. OutputsDataMap _outputinfo;
  52. T_Yolov5sYamlData _yolov5sYamlData;
  53. bool m_isInitialized;
  54. QString m_modelPath;
  55. };
  56. #endif // YOLO5PLUGIN_H

  1. #include "Yolo5Plugin.h"
  2. #include <memory>
  3. #include <iostream>
  4. #include <fstream>
  5. #include <istream>
  6. #include <string>
  7. #include <QPointF>
  8. #include <QRect>
  9. #include <QSizeF>
  10. #include <QTextCodec>
  11. #include <QDebug>
  12. #include <QIcon>
  13. #include <QDir>
  14. #include <QTime>
  15. #include <omp.h>
  16. #include "yaml-cpp/yaml.h"
  17. using namespace std;
  18. static std::mutex mtx;
  19. static omp_lock_t lock_use;
  20. static omp_lock_t s_lock;
  21. #define IMG_LEN 640
  22. #define IMG_LEN_F 640.0f
  23. //https://github.com/fb029ed/yolov5_cpp_openvino/blob/master/cvmart_competition/openvino_cpp_code/my_detector.cpp
  24. bool isFileExist(const QString &fullFilePath)
  25. {
  26. QFileInfo fileInfo(fullFilePath);
  27. if(fileInfo.exists())
  28. {
  29. return true;
  30. }
  31. return false;
  32. }
  33. bool isDirExist(const QString &fullPath)
  34. {
  35. if(fullPath.isEmpty())
  36. {
  37. return false;
  38. }
  39. QDir dir(fullPath);
  40. if(dir.exists())
  41. {
  42. return true;
  43. }
  44. return false;
  45. }
  46. bool parseClass(const string &path, T_Yolov5sYamlData &data)
  47. {
  48. data.names.clear();
  49. data.shows.clear();
  50. try
  51. {
  52. YAML::Node tRoot = YAML::LoadFile(path);
  53. YAML::Node tNames = tRoot["Names"];
  54. for(int i = 0; i < tNames.size(); ++i)
  55. {
  56. std::string name = tNames[i].as<std::string>();
  57. data.names.append(QString::fromStdString(name));
  58. }
  59. if(tRoot["Shows"])
  60. {
  61. for(int i = 0; i < tRoot["Shows"].size(); ++i)
  62. {
  63. bool isShow = tRoot["Shows"][i].as<bool>();
  64. data.shows.append(isShow);
  65. }
  66. }
  67. }
  68. catch (YAML::Exception &e)
  69. {
  70. const char *err_msg = e.what();
  71. qDebug() << "exception caught: {}" << err_msg;
  72. return false;
  73. }
  74. catch (...)
  75. {
  76. qDebug() << "exception caught: unknown";
  77. return false;
  78. }
  79. return true;
  80. }
  81. Yolo5Plugin::Yolo5Plugin()
  82. : m_isInitialized(false), m_modelPath("")
  83. {
  84. }
  85. Yolo5Plugin::~Yolo5Plugin()
  86. {
  87. }
  88. PluginSpec::E_TypeFlag Yolo5Plugin::typeFlag() const
  89. {
  90. return PluginSpec::E_TypeFlag::Defect;
  91. }
  92. QIcon Yolo5Plugin::icon() const
  93. {
  94. return QIcon(":/Yolo5Plugin.png");
  95. }
  96. int Yolo5Plugin::init(const T_DefectSpec &spec)
  97. {
  98. qDebug() << spec.arguments;
  99. /* 判断参数是否有变化,有则重新加载,没有则跳过 */
  100. QString dirPath = spec.arguments.value("-path").toString();
  101. if(m_modelPath == dirPath)
  102. {
  103. qDebug() << "Initialized";
  104. return 0;
  105. }
  106. m_modelPath = dirPath;
  107. m_errmsg = "";
  108. if(!isDirExist(dirPath))
  109. {
  110. m_errmsg = "The model path does not exist";
  111. return -1;
  112. }
  113. /* 模型路径 */
  114. QTextCodec *code = QTextCodec::codecForName("GB2312");
  115. std::string modelPath = code->fromUnicode(dirPath).data();
  116. string xml_path = modelPath + "/yolov5s.xml";
  117. string bin_path = modelPath + "/yolov5s.bin";
  118. string class_path = modelPath + "/yolov5s.yaml";
  119. if(!isFileExist(QString::fromStdString(xml_path)))
  120. {
  121. m_errmsg = "yolov5s.xml does not exist";
  122. return -1;
  123. }
  124. if(!isFileExist(QString::fromStdString(bin_path)))
  125. {
  126. m_errmsg = "yolov5s.bin does not exist";
  127. return -1;
  128. }
  129. if(!isFileExist(QString::fromStdString(class_path)))
  130. {
  131. m_errmsg = "yolov5s.yaml does not exist";
  132. return -1;
  133. }
  134. // qDebug() << "xml_path:" << QString::fromStdString(xml_path);
  135. bool b = parseClass(class_path, _yolov5sYamlData);
  136. if(!b)
  137. {
  138. m_errmsg = "yolov5s.yaml parsing failed";
  139. return -1;
  140. }
  141. /* 推理引擎初始化 */
  142. Core ie;
  143. CNNNetwork network = ie.ReadNetwork(xml_path, bin_path);
  144. network.setBatchSize(1);
  145. /* 输入设置 */
  146. InputInfo::Ptr input_info = network.getInputsInfo().begin()->second;
  147. _input_name = network.getInputsInfo().begin()->first;
  148. input_info->getPreProcess().setResizeAlgorithm(RESIZE_BILINEAR);
  149. input_info->setPrecision(Precision::FP32);
  150. input_info->getInputData()->setLayout(Layout::NCHW);
  151. ICNNNetwork::InputShapes inputShapes = network.getInputShapes();
  152. SizeVector &inSizeVector = inputShapes.begin()->second;
  153. network.reshape(inputShapes);
  154. /* 输出设置 */
  155. _outputinfo = OutputsDataMap(network.getOutputsInfo());
  156. for (auto &output : _outputinfo)
  157. {
  158. output.second->setPrecision(Precision::FP32);
  159. }
  160. /* 加载网络 */
  161. _network = ie.LoadNetwork(network, "CPU");
  162. m_isInitialized = true;
  163. return 0;
  164. }
  165. bool Yolo5Plugin::isInitialized() const
  166. {
  167. return m_isInitialized;
  168. }
  169. int Yolo5Plugin::run(const T_DefectSpec &spec, T_DefectResult &result)
  170. {
  171. if(!isInitialized())
  172. {
  173. m_errmsg = "uninitialized";
  174. return -1;
  175. }
  176. try
  177. {
  178. /* 解析参数 */
  179. double confidence_thr = spec.arguments.value("-confidenceThr").toString().toDouble();
  180. int split_col_num = spec.arguments.value("-split_col_num").toString().toInt();
  181. int split_row_num = spec.arguments.value("-split_row_num").toString().toInt();
  182. /* 大图变ROI */
  183. int maxW = spec.frameParam.width;
  184. int maxH = spec.frameParam.height;
  185. Mat grayMat(maxH, maxW, CV_8UC1, spec.pImage);
  186. if(!spec.rect.isEmpty())
  187. {
  188. maxW = spec.rect.width();
  189. maxH = spec.rect.height();
  190. grayMat = grayMat(cv::Rect(spec.rect.x(), spec.rect.y(),
  191. spec.rect.width(), spec.rect.height()));
  192. }
  193. /* 分割图片 */
  194. vector<Mat> tiles;
  195. vector<int> baseXs;
  196. vector<int> baseYs;
  197. int subWidth = maxW / split_col_num;
  198. int subHeight = maxH / split_row_num;
  199. for(int i = 0; i < split_row_num; i++)
  200. {
  201. for(int j = 0; j < split_col_num; ++j)
  202. {
  203. int baseX = j * subWidth;
  204. int baseY = i * subHeight;
  205. Rect tile_rect = cv::Rect(baseX, baseY, subWidth, subHeight);
  206. tiles.push_back(grayMat(tile_rect));
  207. baseXs.push_back(baseX);
  208. baseYs.push_back(baseY);
  209. }
  210. }
  211. auto calFunction = [ & ](int i, const Mat gray, int baseX, int baseY)
  212. {
  213. int item_size = _yolov5sYamlData.names.size() + 5; //N+5
  214. std::vector<Yolo5Plugin::T_Object> detected_objects;
  215. processFrame(gray, item_size, detected_objects);
  216. mtx.lock();
  217. for(int i = 0; i < detected_objects.size(); ++i)
  218. {
  219. int label = detected_objects.at(i).label;
  220. float prob = detected_objects.at(i).prob;
  221. if(_yolov5sYamlData.shows.value(label, true))
  222. {
  223. QRectF box = detected_objects.at(i).rect;
  224. QString name = _yolov5sYamlData.names.value(label, "None");
  225. /* 置信度阈值 */
  226. if(prob > confidence_thr)
  227. {
  228. /* 合并结果 */
  229. double rectX = box.x() + baseX;
  230. double rectY = box.y() + baseY;
  231. double w = box.width();
  232. double h = box.height();
  233. if(!spec.rect.isEmpty())
  234. {
  235. result.boxVec.push_back(QRectF(rectX + spec.rect.x(), rectY + spec.rect.y(), w, h));
  236. }
  237. else
  238. {
  239. result.boxVec.push_back(QRectF(rectX, rectY, w, h));
  240. }
  241. result.nameVec.push_back(name);
  242. result.confidenceVec.push_back(prob);
  243. }
  244. }
  245. }
  246. mtx.unlock();
  247. };
  248. /* 多线程检测 */
  249. vector<std::thread> threads;
  250. for (int i = 0; i < tiles.size(); i++)
  251. {
  252. threads.push_back(std::thread(calFunction, i, tiles[i], baseXs.at(i), baseYs.at(i)));
  253. }
  254. for (int i = 0; i < threads.size(); i++)
  255. {
  256. threads[i].join();
  257. }
  258. }
  259. catch(cv::Exception &e )
  260. {
  261. const char *err_msg = e.what();
  262. m_errmsg = QString(err_msg);
  263. qDebug() << "exception caught: {}" << err_msg;
  264. return -1;
  265. }
  266. catch(...)
  267. {
  268. m_errmsg = "exception caught: unknown";
  269. qDebug() << "exception caught: unknown";
  270. return -1;
  271. }
  272. return 0;
  273. }
  274. DefectInterface *Yolo5Plugin::newInstance()
  275. {
  276. return new Yolo5Plugin();
  277. }
  278. double Yolo5Plugin::sigmoid(double x)
  279. {
  280. return (1 / (1 + exp(-x)));
  281. }
  282. std::vector<int> Yolo5Plugin::get_anchors(int net_grid)
  283. {
  284. vector<int> anchors(6);
  285. int a80[6] = { 10, 13, 16, 30, 33, 23 };
  286. int a40[6] = { 30, 61, 62, 45, 59, 119 };
  287. int a20[6] = { 116, 90, 156, 198, 373, 326 };
  288. if(net_grid == 80)
  289. {
  290. anchors.insert(anchors.begin(), a80, a80 + 6);
  291. }
  292. else if(net_grid == 40)
  293. {
  294. anchors.insert(anchors.begin(), a40, a40 + 6);
  295. }
  296. else if(net_grid == 20)
  297. {
  298. anchors.insert(anchors.begin(), a20, a20 + 6);
  299. }
  300. return anchors;
  301. }
  302. bool Yolo5Plugin::parse_yolov5(const Blob::Ptr &blob, int net_grid,
  303. float cof_threshold, int item_size,
  304. std::vector<Rect> &o_rect,
  305. std::vector<float> &o_rect_cof,
  306. std::vector<float> &o_labels)
  307. {
  308. vector<int> anchors = get_anchors(net_grid);
  309. LockedMemory<const void> blobMapped = as<MemoryBlob>(blob)->rmap();
  310. const float *output_blob = blobMapped.as<float *>();
  311. size_t gi = net_grid * item_size;
  312. size_t ggi = net_grid * gi;
  313. size_t anchor_n = 3;
  314. // omp_set_num_threads(8);
  315. // #pragma omp parallel for
  316. for (int n = 0; n < anchor_n; ++n)
  317. {
  318. for (int i = 0; i < net_grid; ++i)
  319. {
  320. for (int j = 0; j < net_grid; ++j)
  321. {
  322. double box_prob = output_blob[n * ggi + i * gi + j * item_size + 4];
  323. box_prob = sigmoid(box_prob);
  324. /* 框置信度不满足则整体置信度不满足 */
  325. if (box_prob < cof_threshold)
  326. {
  327. continue;
  328. }
  329. /* 此处输出为中心点坐标,需要转化为角点坐标 */
  330. double x = output_blob[n * ggi + i * gi + j * item_size + 0];
  331. double y = output_blob[n * ggi + i * gi + j * item_size + 1];
  332. double w = output_blob[n * ggi + i * gi + j * item_size + 2];
  333. double h = output_blob[n * ggi + i * gi + j * item_size + 3];
  334. double max_prob = 0;
  335. int idx = 0;
  336. for (int t = 5; t < item_size; ++t)
  337. {
  338. double tp = output_blob[n * ggi + i * gi + j * item_size + t];
  339. tp = sigmoid(tp);
  340. if (tp > max_prob)
  341. {
  342. max_prob = tp;
  343. idx = t - 5;
  344. }
  345. }
  346. double cof = box_prob * max_prob;
  347. if (cof < cof_threshold)
  348. {
  349. continue;
  350. }
  351. x = (sigmoid(x) * 2 - 0.5 + j) * 640.0f / net_grid;
  352. y = (sigmoid(y) * 2 - 0.5 + i) * 640.0f / net_grid;
  353. w = pow(sigmoid(w) * 2, 2) * anchors[2 * n];
  354. h = pow(sigmoid(h) * 2, 2) * anchors[2 * n + 1];
  355. double r_x = x - w / 2;
  356. double r_y = y - h / 2;
  357. Rect rect = Rect(int(r_x), int(r_y), int(w), int(h));
  358. o_rect.push_back(rect);
  359. o_rect_cof.push_back(cof);
  360. o_labels.push_back(idx);
  361. }
  362. }
  363. }
  364. return true;
  365. }
  366. void Yolo5Plugin::resize(const Mat &graySrc, Mat &dst, double &scale)
  367. {
  368. Mat src = graySrc;
  369. cvtColor(src, src, COLOR_GRAY2RGB);
  370. int width = src.cols;
  371. int height = src.rows;
  372. scale = min(640.0 / width, 640.0 / height);
  373. int w = round(width * scale);
  374. int h = round(height * scale);
  375. Mat resize_img;
  376. cv::resize(src, resize_img, Size(w, h)); // 640*X or X*640
  377. int top = 0, bottom = 0, left = 0, right = 0;
  378. if (w > h)
  379. {
  380. top = (w - h) / 2;
  381. bottom = (w - h) - top;
  382. }
  383. else if (h > w)
  384. {
  385. left = (h - w) / 2;
  386. right = (h - w) - left;
  387. }
  388. copyMakeBorder(resize_img, resize_img, top, bottom, left, right, BORDER_CONSTANT, Scalar(114, 114, 114)); // 640*640
  389. cvtColor(resize_img, resize_img, cv::COLOR_BGR2RGB);
  390. dst = resize_img;
  391. }
  392. bool Yolo5Plugin::processFrame(const Mat &gray, int item_size,
  393. std::vector<Yolo5Plugin::T_Object> &detected_objects)
  394. {
  395. /* resize && 颜色通道转换 */
  396. Mat src = gray;
  397. cvtColor(src, src, COLOR_GRAY2RGB);
  398. int width = src.cols;
  399. int height = src.rows;
  400. double scale = min(640.0 / width, 640.0 / height);
  401. int w = round(width * scale);
  402. int h = round(height * scale);
  403. Mat resize_img;
  404. cv::resize(src, resize_img, Size(w, h)); // 640*X or X*640
  405. int top = 0, bottom = 0, left = 0, right = 0;
  406. if (w > h)
  407. {
  408. top = (w - h) / 2;
  409. bottom = (w - h) - top;
  410. }
  411. else if (h > w)
  412. {
  413. left = (h - w) / 2;
  414. right = (h - w) - left;
  415. }
  416. copyMakeBorder(resize_img, resize_img, top, bottom, left, right, BORDER_CONSTANT, Scalar(114, 114, 114)); // 640*640
  417. cvtColor(resize_img, resize_img, cv::COLOR_BGR2RGB);
  418. /* 推断请求和blob填充 */
  419. InferRequest infer_request = _network.CreateInferRequest();
  420. Blob::Ptr frameBlob = infer_request.GetBlob(_input_name);
  421. size_t img_size = IMG_LEN * IMG_LEN;
  422. InferenceEngine::LockedMemory<void> blobMapped = InferenceEngine::as<InferenceEngine::MemoryBlob>(frameBlob)->wmap();
  423. float *blob_data = blobMapped.as<float *>();
  424. // omp_set_num_threads(4);
  425. // #pragma omp parallel for
  426. for (int row = 0; row < IMG_LEN; row++)
  427. {
  428. for (int col = 0; col < IMG_LEN; col++)
  429. {
  430. for (int ch = 0; ch < 3; ch++)
  431. {
  432. blob_data[img_size * ch + row * IMG_LEN + col] = float(resize_img.at<Vec3b>(row, col)[ch]) / 255.0f;
  433. }
  434. }
  435. }
  436. /* 推断执行 */
  437. infer_request.Infer();
  438. /* 获取推断结果 */
  439. int net_grids[3] = {80, 40, 20};
  440. vector<Blob::Ptr> blobs;
  441. for (auto &output : _outputinfo)
  442. {
  443. auto output_name = output.first;
  444. Blob::Ptr blob = infer_request.GetBlob(output_name);
  445. blobs.push_back(blob);
  446. }
  447. float cof_threshold = 0.25;
  448. vector<Rect> o_rect;
  449. vector<float> o_rect_cof;
  450. vector<int> labels;
  451. QTime globalTime2;
  452. globalTime2.start();
  453. for(int i = 0; i < blobs.size(); ++i)
  454. {
  455. if(i < 3)
  456. {
  457. vector<Rect> o_rect_temp;
  458. vector<float> o_rect_cof_temp;
  459. vector<float> o_labels_temp;
  460. parse_yolov5(blobs[i], net_grids[i], cof_threshold, item_size,
  461. o_rect_temp, o_rect_cof_temp, o_labels_temp);
  462. o_rect.insert(o_rect.end(), o_rect_temp.begin(), o_rect_temp.end());
  463. o_rect_cof.insert(o_rect_cof.end(), o_rect_cof_temp.begin(), o_rect_cof_temp.end());
  464. labels.insert(labels.end(), o_labels_temp.begin(), o_labels_temp.end());
  465. }
  466. }
  467. /* NMS去除同一个物体多余的框 */
  468. float nms_area_threshold = 0.45;
  469. vector<int> final_id;
  470. dnn::NMSBoxes(o_rect, o_rect_cof, cof_threshold, nms_area_threshold, final_id);
  471. int num = final_id.size();
  472. for (int i = 0; i < num; ++i)
  473. {
  474. int label = labels[final_id[i]];
  475. Rect resize_rect = o_rect[final_id[i]];
  476. int find_col = int(max(resize_rect.x - left, 0) / scale);
  477. int find_row = int(max(resize_rect.y - top, 0) / scale);
  478. int find_w = int(resize_rect.width / scale);
  479. int find_h = int(resize_rect.height / scale);
  480. QRectF rawrect(find_col, find_row, find_w, find_h);
  481. float cof = o_rect_cof[final_id[i]];
  482. detected_objects.push_back(T_Object{cof, label, rawrect});
  483. }
  484. return true;
  485. }
  486. #if QT_VERSION < 0x050000
  487. Q_EXPORT_PLUGIN2(Yolo5Plugin, Yolo5Plugin)
  488. #endif // QT_VERSION < 0x050000

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/819555
推荐阅读
相关标签
  

闽ICP备14008679号