C++ 桌面应用程序


项目概述

../_images/ico.png

DaoAI World Example 项目是一个基于Qt的C++应用程序,用于加载、处理和推理图像数据,并将结果可视化。
该应用程序使用OpenCV库进行图像处理,使用nlohmann::json库解析和操作JSON数据,并通过DaoAI深度学习SDK进行模型推理。
项目下载请点击: DaoAI World Example


主要功能

1. 加载和显示图像:用户可以通过界面加载图像,并在UI上显示。
2. 加载和显示模型:用户可以加载模型文件,并准备进行推理。
3. 执行推理:使用指定的模型对图像进行推理,并将结果保存为JSON文件。
4. 绘制图形:根据推理结果在图像上绘制多边形和关键点,并保存结果图像。
5. 日志记录:将重要操作记录在日志窗口中,便于用户跟踪和调试。

../_images/mainwindow.png

文件结构

头文件

  • daoai_world_example.h: 定义了主窗口类daoai_world_example,包含UI组件和槽函数的声明。

  • inference.h: 声明了图像和模型处理相关的函数。

  • ui_daoai_world_example.h: 声明了Ui_daoai_world_exampleClass类,构建UI主窗口和控件。

源文件

  • daoai_world_example.cpp: 实现了主窗口类的构造函数、析构函数和槽函数。

  • mainwindow.cpp: 程序入口,创建并显示主窗口。

  • inference.cpp: 实现了图像和模型处理相关的函数,包括图像加载、推理、结果绘制和保存。


依赖项

1. Qt 6.x 或更高版本。
2. OpenCV 4.x 或更高版本。
3. nlohmann/json 3.x 或更高版本。
4. DaoAI SDK。

使用指南

环境设置

1. 安装Qt和Visual Studio 2019(或更高版本)。
下载 Qt6Visual Studio 2019 ,勾选必要组件,依次进行安装。
../_images/Qt_install.png ../_images/VS_install.png

















2. 安装OpenCV库。
下载 OpenCV–4.10.0 ,解压到C盘根目录,例:C:\OpenCV\.
3. 安装nlohmann/json库。
nlohmann/json 已配置在项目根目录下,目标路径:$(SolutionDir)\Json.
4. 安装DaoAI SDK。
DaoAI SDK 相关文件已配置在项目根目录下,目标路径:$(SolutionDir)\3rdparty;$(SolutionDir)\bin;$(SolutionDir)\include.

编译和运行

1. 打开Visual Studio 2019并加载项目文件。
2. 配置项目,确保所有依赖库已正确链接。
  • 打开项目属性,配置Debugging,“Inherit from parent or project defaults“ 取消勾选,Environment 目标路径包含:$(SolutionDir)\bin;$(SolutionDir)\3rdparty;C:\OpenCV\opencv\build\x64\vc16\bin;C:\Qt\6.6.1\msvc2019_64\bin。
../_images/Debugging.png
  • 配置VC++ Directories,Include Directories 目标路径包含:C:\OpenCV\opencv\build\include\opencv2;C:\OpenCV\opencv\build\include。Library Directories 目标路径包含:C:\OpenCV\opencv\build\x64\vc16lib。
../_images/VC%2B%2B.png
  • 配置C/C++ General,Additional Include Directories 目标路径包含:$(SolutionDir)Jsonincludeinclude;$(SolutionDir)include。
../_images/C%2B%2B.png
  • 配置Linker General,Additional Library Directories 目标路径包含:$(SolutionDir)bin。
../_images/Linker.png
  • 配置Linker General,Additional Dependencies 目标路径包含:daoai_dl_sdk.lib;opencv_world4100.lib。
../_images/input.png
3. X64 release 编译,运行项目。
../_images/run.png

功能说明

主窗口

主窗口包含以下组件:
  • 文本输入框: 用于输入图像和模型文件路径。
  • 按钮:

  • pushButton: 触发推理。

  • pushButton_load: 加载图像文件。

  • pushButton_load_2: 加载模型文件。
  • 标签: 显示加载的图像和推理结果。

图像加载和显示

  • on_pushButton_load_clicked: 打开文件对话框选择图像文件,并在label_3上显示。
  • on_pushButton_load_model_clicked: 打开文件对话框选择模型文件,并在文本框中显示路径。

推理

  • on_pushButton_clicked: 执行推理流程,调用executeInference函数进行模型推理,并显示结果图像。

日志记录

  • appendLog: 将日志信息添加到日志窗口。


源码解析

inference.cpp/主窗口类

文件包含的库

  • 该文件包含了标准C++库、OpenCV库、JSON处理库(nlohmann/json)和深度学习SDK(dlsdk)的头文件。

 1#include <windows.h>
 2#include <iostream>
 3#include <thread>
 4#include <future>
 5#include <fstream>
 6#include <vector>
 7#include <string>
 8#include <opencv2/opencv.hpp>
 9#include <nlohmann/json.hpp>
10#include <dlsdk/model.h>
11#include "daoai_world_example.h"

命名空间

  • 使用std命名空间和cv命名空间,以便简化代码中的命名。

1using namespace std;
2using namespace cv;

JSON别名

  • 为nlohmann的JSON库起别名为json,方便在代码中使用。

1// json another name
2using json = nlohmann::json;

createDirectory 函数

  • 此函数用于创建指定路径的目录。它接受两个参数:目录路径和daoai_world_example实例的指针。 若目录已存在或创建成功,会在控制台和UI日志中记录相关信息。

 1// Create "./Data" folder
 2void createDirectory(const std::string& directoryPath, daoai_world_example* example) {
 3        if (!CreateDirectoryA(directoryPath.c_str(), NULL)) {
 4                if (GetLastError() == ERROR_ALREADY_EXISTS) {
 5                        std::cout << "Directory already exists: " << "\"" + directoryPath + "\"" << std::endl;
 6                        example->appendLog("Directory already exists: " + QString::fromStdString("\"" + directoryPath + "\""));
 7                }
 8                else {
 9                        std::cerr << "Failed to create directory: " << GetLastError() << std::endl;
10                        example->appendLog("Failed to create directory: " + QString::number(GetLastError()));
11                }
12        }
13        else {
14                std::cout << "Directory created successfully: " << directoryPath << std::endl;
15                example->appendLog("Directory created successfully: " + QString::fromStdString(directoryPath));
16        }
17}

drawShapes 函数

  • 此函数用于在图像上绘制多边形和关键点。它从JSON数据中读取形状信息,并根据形状的标签选择不同颜色进行绘制。

 1// Draw polygon and keypoint
 2void drawShapes(cv::Mat& image, const json& jsonData) {
 3    for (const auto& shape : jsonData["shapes"]) {
 4        if (shape["shape_type"] == "polygon") {
 5            // Processing polygon
 6            std::vector<cv::Point> points;
 7            for (const auto& point : shape["points"]) {
 8                points.emplace_back(cv::Point(static_cast<int>(point[0]), static_cast<int>(point[1])));
 9            }
10
11            // Select color
12            cv::Scalar color(255, 255, 255);
13            if (shape["label"].is_string() && !shape["label"].empty()) {
14                if (shape["label"] == "fan") {
15                    color = cv::Scalar(0, 0, 255); // Red
16                }
17                else if (shape["label"] == "zheng") {
18                    color = cv::Scalar(0, 255, 0); // Green
19                }
20                else {
21                    color = cv::Scalar(255, 0, 0); // Blue
22                }
23            }
24
25            // Creates a group of points to draw polygons
26            std::vector<std::vector<cv::Point>> pts = { points };
27
28            // Draw polygon
29            cv::polylines(image, pts, true, color, 2);
30        }
31        else if (shape["shape_type"] == "point") {
32            // Processing keypoint
33            cv::Point center(static_cast<int>(shape["points"][0][0]), static_cast<int>(shape["points"][0][1]));
34
35            // Select color
36            cv::Scalar color(255, 255, 255);
37            if (shape["label"].is_string() && !shape["label"].empty()) {
38                char firstChar = shape["label"].get<std::string>().at(0);
39                if (firstChar == 'z' || firstChar == 'Z') {
40                    color = cv::Scalar(0, 255, 0); // Green
41                }
42                else if (firstChar == 'f' || firstChar == 'F') {
43                    color = cv::Scalar(0, 0, 255); // Red
44                }
45                else {
46                    color = cv::Scalar(255, 0, 0); // Blue
47                }
48            }
49
50            // Draw keypoint
51            cv::circle(image, center, 5, color, cv::FILLED);
52        }
53    }
54}

drawImage 函数

  • 此函数负责读取JSON文件和图像文件,调用drawShapes函数在图像上绘制形状,然后将修改后的图像保存到指定路径。

 1int drawImage(const std::string& jsonPath, const std::string& imagePath, const std::string& savePath, daoai_world_example* example) {
 2    // Open and parse the JSON file
 3    std::ifstream inputFile(jsonPath);
 4    if (!inputFile.is_open()) {
 5        std::cerr << "Cannot open " << jsonPath << std::endl;
 6        example->appendLog("Cannot open " + QString::fromStdString(jsonPath));
 7        return 1;
 8    }
 9
10    json jsonData;
11    inputFile >> jsonData;
12
13    // Read image file
14    cv::Mat image = cv::imread(imagePath);
15    if (image.empty()) {
16        std::cerr << "Cannot read " << imagePath << std::endl;
17        example->appendLog("Cannot read " + QString::fromStdString(imagePath));
18        return 1;
19    }
20
21    // Draw shape
22    drawShapes(image, jsonData);
23
24    // Display image
25    // cv::imshow("Image with Shapes and KeyPoints", image);
26    // cv::waitKey(0);
27
28    // Save image
29    if (cv::imwrite(savePath, image)) {
30        std::cout << "Image has been saved as: " << "\"" + savePath + "\"" << std::endl;
31        example->appendLog("Image has been saved as: " + QString::fromStdString("\"" + savePath + "\""));
32    }
33    else {
34        std::cerr << "Cannot save image " << savePath << std::endl;
35        example->appendLog("Cannot save image " + QString::fromStdString(savePath));
36    }
37
38    return 0;
39}

readPngImageFile 函数

  • 此函数用于读取PNG图像文件并将其转换为uint8_t类型的向量。

 1/*
 2 * @brief Reads PNG image file into a vector of uint8_t.
 3 *
 4 * @param filePath The path to the PNG file.
 5 * @return std::vector<uint8_t> A vector containing the uint8 data from the image file.
 6 *                              Returns an empty vector if the file could not be opened.
 7 */
 8std::vector<uint8_t> readPngImageFile(const std::string& filePath) {
 9    cv::Mat image = cv::imread(filePath, cv::IMREAD_UNCHANGED);
10
11    /*
12    if (image.empty()) {
13        std::cerr << "Could not read image file " << filePath << std::endl;
14        return {};
15    }
16    */
17
18    if (image.empty()) {
19        throw std::runtime_error("Could not read image file " + filePath);
20        return {};
21    }
22
23    std::vector<uint8_t> imageBuffer(image.data, image.data + image.total() * image.elemSize());
24    return imageBuffer;
25}

readImageBinaryFile 函数

  • 此函数用于读取二进制图像文件并将其转换为uint8_t类型的向量。

 1/*
 2 * @brief Reads binary image file into a vector of uint8_t.
 3 *
 4 * @param filePath The path to the binary file.
 5 * @return std::vector<uint8_t> A vector containing the binary data from the image file.
 6 *                              Returns an empty vector if the file could not be opened.
 7 */
 8std::vector<uint8_t> readImageBinaryFile(const std::string& filePath) {
 9    std::ifstream inputFile(filePath, std::ios::binary);
10    if (!inputFile) {
11        std::cerr << "Cannot open " << filePath << "\n";
12        return {};
13    }
14
15    std::vector<uint8_t> buffer((std::istreambuf_iterator<char>(inputFile)), std::istreambuf_iterator<char>());
16    inputFile.close();
17
18    return buffer;
19}

inference 函数

  • 此函数负责初始化深度学习模型,读取图像数据,进行推理并将结果写入JSON文件。

 1int inference(const std::string& modelPath, const std::string& imagePath, const std::string& jsonPath, daoai_world_example* example) {
 2    try
 3    {
 4        DaoAI::DeepLearning::initialize();
 5        example->appendLog("DeepLearning: Initialize succeed!");
 6
 7        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 8        // This part of the code demonstrating on an image from a binary file, you can replace the image_buffer by your own image data (i.e.: data buffer from opencv)
 9        const int image_height = 1200;
10        const int image_width = 1920;
11
12        std::vector<uint8_t> image_buffer = readPngImageFile(imagePath);
13        if (image_buffer.size() == 0)
14        {
15            std::cerr << "Image buffer is not read properly, returns an empty vector" << std::endl;
16            example->appendLog("Image buffer is not read properly, returns an empty vector");
17            return 1;
18        }
19        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
20
21        // init model
22        DaoAI::DeepLearning::Model model;
23        example->appendLog("Initialized deeplearning model");
24
25        // load model
26        model.loadNestedZip(modelPath, DaoAI::DeepLearning::Device_Type::GPU, -1);
27        example->appendLog("Loaded deeplearning model");
28
29        DaoAI::DeepLearning::Image daoai_image(image_height, image_width, DaoAI::DeepLearning::Image::Type::BGR, &image_buffer[0]);
30
31        // get inference
32        auto prediction = model.inferenceJSON(daoai_image);
33        example->appendLog("Inference completed");
34
35        // write to json file
36        std::ofstream fout(jsonPath);
37        fout << prediction << std::endl;
38        fout.close();
39
40        // unload model
41        model.unload();
42    }
43    catch (const std::exception& e)
44    {
45        std::cerr << e.what() << std::endl;
46        return 1;
47    }
48    return 0;
49}

executeInference 函数

  • 此函数单线程执行推理任务。它创建必要的目录,调用inference函数进行推理,并在图像上绘制结果。

 1// Single-threaded, may cause the cursor to become unresponsive during inference
 2std::string executeInference(const std::string& dataPath, const std::string& modelPath, daoai_world_example* example) {
 3
 4    example->appendLog("Satrt execute inference");
 5
 6    // get root path to the model and image
 7    std::string data_path = dataPath;
 8    std::string model_zip_path = modelPath;
 9    std::string root = "./Data";
10    std::string json_path = root + "/daoai_1.json";
11    std::string image_path = root + "/daoai_1.jpg";
12
13    // The 'example' is a valid pointer to daoai_world_example instance
14    //daoai_world_example* example = new daoai_world_example(); // instantiation
15    createDirectory(root, example);
16
17    int result = inference(model_zip_path, data_path, json_path, example); // get the return value of inference function
18
19    if (!result) {
20        drawImage(json_path, data_path, image_path, example);
21    }
22    else {
23        return "1";
24    }
25
26    //delete example; // Delete example
27    return image_path;
28}

executeInference_multi 函数

  • 此函数多线程执行推理任务。它通过线程异步执行目录创建和推理过程,并在推理完成后在图像上绘制结果。

 1// Multi-threaded, the cursor can do other things during inference
 2std::string executeInference_multi(const std::string& dataPath, const std::string& modelPath, daoai_world_example* example) {
 3    // create a promise object
 4    std::promise<int> promise;
 5
 6    // getting the future object
 7    std::future<int> future = promise.get_future();
 8
 9    example->appendLog("Satrt execute inference");
10
11    // get root path to the model and image
12    std::string data_path = dataPath;
13    std::string model_zip_path = modelPath;
14    std::string root = "./Data";
15    std::string json_path = root + "/daoai_1.json";
16    std::string image_path = root + "/daoai_1.jpg";
17
18    // The 'example' is a valid pointer to daoai_world_example instance
19    //daoai_world_example* example = new daoai_world_example(); // instantiation
20    std::thread t_createDirectory(createDirectory, root, example);
21    t_createDirectory.join();
22
23    // Create a thread that passes three strings and the daoai_world_example instance
24    std::thread t_inference([&promise, model_zip_path, data_path, json_path, example]() {
25        int result = inference(model_zip_path, data_path, json_path, example);
26        promise.set_value(result); // Set the return value at the end of the thread
27        });
28    t_inference.join();
29    int result = future.get(); // get the return value of a thread function
30
31    if (!result) {
32        std::thread t_drawImage(drawImage, json_path, data_path, image_path, example);
33        t_drawImage.join();
34    }
35    else {
36        return "1";
37    }
38
39    //delete example; // Delete example
40    return image_path;
41}

daoai_world_example.cpp/推理相关函数

文件包含的库

  • 此文件包含了项目的头文件、Qt库文件和推理功能头文件。

1#include "daoai_world_example.h"
2#include "ui_daoai_world_example.h"
3#include <inference.h>
4#include <QFileDialog>
5#include <QPixmap>
6#include <QDebug>
7#include <QEventLoop>
8#include <QTimer>
9#include <string>

构造函数与析构函数

  • 构造函数初始化UI组件,并设置按钮点击事件的连接。析构函数用于清理资源。

 1daoai_world_example::daoai_world_example(QWidget* parent)
 2    : QMainWindow(parent), scaleFactor(1.0)
 3{
 4    ui.setupUi(this);
 5
 6    // Set the window icon
 7    setWindowIcon(QIcon("icon/01.ico"));
 8
 9    disconnect(ui.pushButton, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_clicked);
10    disconnect(ui.pushButton_load, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_load_clicked);
11    disconnect(ui.pushButton_load_2, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_load_model_clicked);
12    connect(ui.pushButton, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_clicked);
13    connect(ui.pushButton_load, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_load_clicked);
14    connect(ui.pushButton_load_2, &QPushButton::clicked, this, &daoai_world_example::on_pushButton_load_model_clicked);
15
16    // initialize the mouse press state
17    mousePressed = false;
18}
19
20daoai_world_example::~daoai_world_example() {
21}

appendLog 函数

  • 此函数将日志消息追加到UI的日志文本框中。

1void daoai_world_example::appendLog(const QString& logMessage) {
2    ui.textEditLog->append(logMessage);
3}

on_pushButton_clicked 函数

  • 此函数处理推理按钮的点击事件,调用executeInference函数执行推理任务,并显示推理结果图像。

 1void daoai_world_example::on_pushButton_clicked() {
 2
 3    qDebug() << "PushButton Clicked";
 4    QString fileName = QString::fromStdString(executeInference(ui.lineEdit->text().toStdString(), ui.lineEdit_2->text().toStdString(), this));
 5    if (!fileName.isEmpty()) {
 6
 7        pixmap.load(fileName);
 8        if (!pixmap.isNull()) {  // check that the image loaded successfully
 9            scaleFactor = 1.0;  // reset the scaling factor
10            ui.label->setPixmap(pixmap);
11            //ui.label->resize(pixmap.size());  // resize the QLabel to the image size
12            ui.label->setScaledContents(true);  // adapt the image to the size of the QLabel
13            ui.label->update(); // Force repaint
14            QEventLoop loop;
15            QTimer::singleShot(0, &loop, &QEventLoop::quit);
16            loop.exec();
17        }
18        else {
19            appendLog("Inference result failed to load");
20        }
21
22    }
23    else {
24        appendLog("Inference failed");
25    }
26}

on_pushButton_load_clicked 函数

  • 此函数处理加载图像按钮的点击事件,打开文件对话框选择图像文件,并显示选择的图像。

 1void daoai_world_example::on_pushButton_load_clicked() {
 2
 3    qDebug() << "PushButton Load Clicked ";
 4    QString fileName = QFileDialog::getOpenFileName(this, "Open Image File", "", "Images (*.png *.xpm *.jpg)");
 5    if (!fileName.isEmpty()) {
 6        ui.lineEdit->setText(fileName);  // Displays the file path in QLineEdit
 7
 8        pixmap.load(fileName);
 9        if (!pixmap.isNull()) {  // check that the image loaded successfully
10            scaleFactor = 1.0;  // reset the scaling factor
11            ui.label_3->setPixmap(pixmap);
12            //ui.label->resize(pixmap.size());  // resize the QLabel to the image size
13            ui.label_3->setScaledContents(true);  // adapt the image to the size of the QLabel
14            ui.label_3->update(); // Force repaint
15            QEventLoop loop;
16            QTimer::singleShot(0, &loop, &QEventLoop::quit);
17            loop.exec();
18            appendLog("Image loaded successfully");
19        }
20        else {
21            appendLog("Image loaded fail");
22        }
23
24    }
25    else {
26        // The user is prompted that the image path is invalid
27        ui.lineEdit->setText("Invalid image path");
28    }
29
30}

on_pushButton_load_model_clicked 函数

  • 此函数处理加载图像按钮的点击事件,打开文件对话框选择图像文件,并显示选择的图像。

 1void daoai_world_example::on_pushButton_load_model_clicked() {
 2
 3    qDebug() << "PushButton Load Model Clicked ";
 4    QString fileName = QFileDialog::getOpenFileName(this, "Open Model File", "", "Zip Files (*.zip);;Model Files (*.pt)");
 5    if (!fileName.isEmpty()) {
 6        ui.lineEdit_2->setText(fileName);  // Displays the file path in QLineEdit
 7        appendLog("Model loaded successfully");
 8
 9    }
10    else {
11        // The user is prompted that the image path is invalid
12        ui.lineEdit_2->setText("Invalid image path");
13    }
14
15}

鼠标事件处理函数

  • 这四个函数处理鼠标按下、移动、释放和滚轮事件,实现图像的拖动和缩放功能。

 1void daoai_world_example::mousePressEvent(QMouseEvent* event) {
 2    if (event->button() == Qt::LeftButton) {
 3        lastPos = event->position().toPoint();  // Get the position where the mouse is pressed
 4        mousePressed = true;
 5    }
 6}
 7
 8void daoai_world_example::mouseMoveEvent(QMouseEvent* event) {
 9    if (mousePressed) {
10        QPoint currentPos = event->position().toPoint();  // Get the current position of the mouse
11        int dx = currentPos.x() - lastPos.x();
12        int dy = currentPos.y() - lastPos.y();
13        lastPos = currentPos;
14
15        // Moving image
16        QPoint new_pos = ui.label->pos() + QPoint(dx, dy);
17        ui.label->move(new_pos);
18    }
19}
20
21void daoai_world_example::mouseReleaseEvent(QMouseEvent* event) {
22    if (event->button() == Qt::LeftButton) {
23        mousePressed = false;
24    }
25}
26
27void daoai_world_example::wheelEvent(QWheelEvent* event) {
28    // Check if the cursor is inside the QLabel and add a buffer
29    const int buffer = -100; // Adjust this value to increase the buffer size
30    QRect labelGeometry = ui.label->geometry().adjusted(-buffer, -buffer, buffer, buffer);
31
32    QPoint cursorPos = mapFromGlobal(QCursor::pos());
33    if (!labelGeometry.contains(cursorPos)) {
34        return; // If the cursor is not inside the QLabel or within the buffer, it returns and the wheel event is not handled
35    }
36
37    // Get the rolling Angle of the roller in 1/8 degree
38    int delta = event->angleDelta().y();
39
40    // zoom factor
41    qreal scaleFactorDelta = (delta > 0) ? 1.15 : 1 / 1.15;
42
43    // Update the scaling factor
44    qreal newScaleFactor = scaleFactor * scaleFactorDelta;
45    newScaleFactor = qMax(0.1, qMin(newScaleFactor, 4.0));
46
47    // Calculate the change in the scaling ratio
48    qreal factorChange = newScaleFactor / scaleFactor;
49
50    // Update the scaling factor
51    scaleFactor = newScaleFactor;
52
53    // zoom image
54    QSize newSize = pixmap.size() * scaleFactor;
55    QPixmap scaledPixmap = pixmap.scaled(newSize, Qt::KeepAspectRatio);
56    ui.label->setPixmap(scaledPixmap);
57    ui.label->resize(scaledPixmap.size());
58
59    // Resize the QLabel to the window size to fit the scaled image
60    ui.label->adjustSize();
61
62    // Converts the mouse position from window coordinates to image coordinates
63    QPoint imgPos = cursorPos - ui.label->pos();
64
65    // The new image position is computed to keep the cursor position constant in the image part
66    QPointF newLabelPos = cursorPos - imgPos * factorChange;
67    ui.label->move(newLabelPos.toPoint());
68}

示例运行

1. 启动程序。
2. 点击“加载图像”按钮选择图像文件。
3. 点击“加载模型”按钮选择模型文件。
4. 点击“推理”按钮执行推理,结果将显示在UI上。

日志和错误处理

  • 程序会在日志窗口中显示操作日志和错误信息,便于用户跟踪和调试。如遇到问题,可根据日志信息排查原因。


许可证

  • 此项目遵循MIT许可证。详见LICENSE文件。

  • 此项目所引用的DaoAI深度学习SDK需要授权,请联系我们获取授权文件。