TensorRt(5)动态尺寸输入的分割模型测试

文章目录

  • 1、固定输入尺寸逻辑
  • 2、动态输入尺寸
    • 2.1、模型导出
    • 2.2、推理测试
    • 2.3、显存分配问题
    • 2.4、完整代码

这里主要说明使用TensorRT进行加载编译优化后的模型engine进行推理测试,与前面进行目标识别、目标分类的模型的网络输入是固定大小不同,导致输入维度不能直接获取需要自己手动调整的问题。

1、固定输入尺寸逻辑

基本逻辑如下:

  • 读取engine文件到内存
  • 使用TensorRT运行时IRuntime序列化一个引擎ICudaEngine,在创建一个上下文对象IExecutionContext
  • 从引擎中ICudaEngine获取输入、输出的纬度和数据类型,并分配显存
  • 将输入从host内存中拷贝到输入device显存中
  • 利用创建的上下文对象IExecutionContext执行推理
  • 将推理结果从输出device显存拷贝到host内存中

至于显存分配,根据engine是可以获取网络输入输出的尺寸的。以前面 【TensorRt(3)mnist示例中的C++ API】 博客中的简要代码为例说推理代码路程:

int simple2()
{/// 2.1  加载engine到内存// .... 省略std::vector<char> buf(buflen);// .... /// 2.2 反序列化std::unique_ptr<IRuntime> runtime{ createInferRuntime(sample::gLogger.getTRTLogger()) };auto mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(buf.data(),buf.size()),[](nvinfer1::ICudaEngine* p) {delete p; });// inference上下文auto context = std::unique_ptr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());// 网络输入、输出信息  auto mInputDims = mEngine->getBindingDimensions(0);     // 部署使用 [1,1,28,28]auto mOutputDims = mEngine->getBindingDimensions(1);    // 部署使用 [1,10]int inputH = mInputDims.d[2];int inputW = mInputDims.d[3];//----------  整个网络输入只有1个,输出只有1个,且均为float类型,分配cuda显存std::vector<void*> bindings(mEngine->getNbBindings());for (int i = 0; i < bindings.size(); i++) {nvinfer1::DataType type = mEngine->getBindingDataType(i);// 明确为 floatsize_t volume = sizeof(float) * std::accumulate(dims.d,dims.d + dims.nbDims,1,std::multiplies<size_t>());CHECK(cudaMalloc(&bindings[i],volume));}// 加载一个random imagesrand(unsigned(time(nullptr)));std::vector<uint8_t> fileData(inputH * inputW);// 省略.... //----------  输入host数据类型从uint8_t转换为float, 这里明确知道 1*1*28*28std::vector<float> hostDataBuffer(1 * 1 * 28 * 28); for (int i = 0; i < inputH * inputW; i++) {hostDataBuffer[i] = 1.0 - float(fileData[i] / 255.0);}//----------  将图像数据从host空间拷贝到device空间CHECK(cudaMemcpy(bindings[0],static_cast<const void*>(hostDataBuffer.data()),hostDataBuffer.size() * sizeof(float), cudaMemcpyHostToDevice));//----------  excution执行推理bool status = context->executeV2(bindings.data());///  2.3 处理推理结果数据,//----------  将推理结果从device空间拷贝到host空间std::vector<float> pred(1 * 10); // 这里明确知道 1*10)CHECK(cudaMemcpy(static_cast<void*>(pred.data()),bindings[1], pred.size() * sizeof(float),cudaMemcpyDeviceToHost));// .... 省略
}

2、动态输入尺寸

以paddle中的语义语义分割模型 OCRNet + backbone HRNet_w18 为例进行说明测试。

2.1、模型导出

默认训练模型导出推理模型是不带softmax和argmax层的,为避免后续实现效率降低,添加这两层之再导出推理模型,使用paddle2onnx导出onnx模型,

paddle2onnx --model_dir saved_inference_model \--model_filename model.pdmodel \--params_filename model.pdiparams \--save_file model.onnx \--enable_dev_version True       # --opset_version 12 # default 9

使用netron工具查看输入和输出的尺寸信息截图如下
在这里插入图片描述
输入尺寸为 [n, 3, h, w],原始输出为[n,c,h,w],由于使用soft和argmax,输出直接为[1, h, w]。输出和输入图像宽高一致,输出的每个像素位置就是像素分类的类别数。

之后使用 trtexec 将onnx模型编译优化导出为engine类型,由于是动态输入,因此指定了输入尺寸范围和最优尺寸。

trtexec.exe --onnx=model.onnx --explicitBatch --fp16 --minShapes=x:1x3x540x960 --optShapes=x:1x3x720x1280 --maxShapes=x:1x3x1080x1920 --saveEngine=model.trt

2.2、推理测试

(0)基本类型数据准备
我们仅使用#include "NvInfer.h" ,来使用TensorRT sdk,定义几个需要的宏和对象

#define CHECK(status)                                                            \do                                                                           \{                                                                            \auto ret = (status);                                                     \if (ret != 0)                                                            \{                                                                        \std::cerr << "Cuda failure: " << ret << std::endl;                   \abort();                                                             \}                                                                        \} while (0)
class Logger : public nvinfer1::ILogger
{
public:Logger(Severity severity = Severity::kWARNING) : severity_(severity) {}virtual void log(Severity severity, const char* msg) noexcept override{// suppress info-level messagesif(severity <= severity_)std::cout << msg << std::endl;}nvinfer1::ILogger& getTRTLogger() noexcept{return *this;}
private:Severity severity_;
};
struct InferDeleter
{template <typename T>void operator()(T* obj) const{delete obj;}
};template <typename T>
using SampleUniquePtr = std::unique_ptr<T, InferDeleter>;

(1)加载模型

Logger logger(nvinfer1::ILogger::Severity::kVERBOSE);/*trtexec.exe --onnx=inference_model\model.onnx --explicitBatch --fp16 --minShapes=x:1x3x540x960 --optShapes=x:1x3x720x1280 --maxShapes=x:1x3x1080x1920 --saveEngine=model.trt*/std::string trtFile = R"(C:\Users\wanggao\Desktop\123\inference_model_0\model.trt)";//std::string trtFile = "model.test.trt";std::ifstream ifs(trtFile, std::ifstream::binary);if(!ifs) {return false;}ifs.seekg(0, std::ios_base::end);int size = ifs.tellg();ifs.seekg(0, std::ios_base::beg);std::unique_ptr<char> pData(new char[size]);ifs.read(pData.get(), size);ifs.close();// engine模型//SampleUniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(logger.getTRTLogger())};//auto mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(//    runtime->deserializeCudaEngine(pData.get(), size), InferDeleter());//auto context = SampleUniquePtr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());std::shared_ptr<nvinfer1::ICudaEngine> mEngine;{SampleUniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(logger.getTRTLogger())};mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(pData.get(), size), InferDeleter());}auto context = SampleUniquePtr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());

(2)输入
将RGB三通道图像转换为NCHW格式数据,数据类型为float。

    // 输入 前处理//cv::Mat img = cv::imread("dog.jpg");cv::Mat img = cv::imread(R"(C:\Users\wanggao\Desktop\123\data\test\t.jpg)");cv::Mat blob = cv::dnn::blobFromImage(img, 1 / 255., img.size(), {0,0,0}, true, false);//blob = blob * 2 - 1;  // 测试使用,可以不归一化

(3)显存分配
不同于固定输入,通过engine获取尺寸并分配显存大小

	// 固定输入尺寸的显存分配方式std::vector<void*> bindings(mEngine->getNbBindings());for (int i = 0; i < bindings.size(); i++) {nvinfer1::DataType type = mEngine->getBindingDataType(i);nvinfer1::Dims dims = mEngine->getBindingDimensions(i);//size_t volume = //   sizeof(float) * std::accumulate(dims.d,dims.d + dims.nbDims,1,std::multiplies<size_t>());size_t volume = std::accumulate(dims.d,dims.d + dims.nbDims,1,std::multiplies<size_t>());switch (type){case nvinfer1::DataType::kINT32:case nvinfer1::DataType::kFLOAT: volume *= 4; break;  // 明确为类型 floatcase nvinfer1::DataType::kHALF: volume *= 2; break;case nvinfer1::DataType::kBOOL:case nvinfer1::DataType::kINT8:default:break;}CHECK(cudaMalloc(&bindings[i],volume));}

这里通过模型获取的输入类型为float尺寸为[-1,3,1,1]、输出类型为int32尺寸为[-1,-1,-1],即获取不到尺寸信息。所以,只能根据输入的尺寸来分配显存大小。(文后说明在实际推理中应该如何分配)

	// 设置网络的输入尺寸context->setBindingDimensions(0, nvinfer1::Dims4{1, 3 , img.rows, img.cols});// 分配显存std::vector<void*> bindings(mEngine->getNbBindings()); //auto type1 = mEngine->getBindingDataType(0);  // kFLOAT  float//auto type2 = mEngine->getBindingDataType(1);  // kINT32  intCHECK(cudaMalloc(&bindings[0], sizeof(float) * 1 * 3 * img.rows * img.cols*3));    // n*3*h*wCHECK(cudaMalloc(&bindings[1], sizeof(int) * 1 * 1 * img.rows * img.cols*3));   // n*1*h*w

注意,必须通过context->setBindingDimensions()设置网络的输入尺寸,否则网络在推理时报错,即输入维度未指定,导出网络输出无结果。

3: [executionContext.cpp::nvinfer1::rt::ShapeMachineContext::resolveSlots::1541] Error Code 3: API Usage Error (Parameter check failed at: executionContext.cpp::nvinfer1::rt::ShapeMachineContext::resolveSlots::1541, condition: allInputDimensionsSpecified(routine)
)
9: [executionContext.cpp::nvinfer1::rt::ExecutionContext::executeInternal::564] Error Code 9: Internal Error (Could not resolve slots: )

(4)推理
将前处理后的图片数据拷贝到显存中,之后进行推理,之后将推理结果数据从显存拷贝到内存中

    cv::Mat pred(img.size(), CV_32SC1, {255,255,255}); // 用于输出//cv::reduceArgMax()  //opencv 4.8.0//buffers.copyInputToDevice();CHECK(cudaMemcpy(bindings[0], static_cast<const void*>(blob.data), 3 * img.rows * img.cols * sizeof(float), cudaMemcpyHostToDevice));context->executeV2(bindings.data());// buffers.copyOutputToHost()CHECK(cudaMemcpy(static_cast<void*>(pred.data), bindings[1], pred.total() * sizeof(int), cudaMemcpyDeviceToHost));

(5)结果数据展示(后处理)
这里仅显示,运行截图如下
在这里插入图片描述

2.3、显存分配问题

在示例中,我们根据图片大小来分配显存,实际应用将进行多次推理,有多种方案:

  • 1、根据实际输入大小,每次进行动态分配(使用完后释放原已分配显存)
  • 2、在1基础上,如果显存不够再分配(分配前释放原已分配显存)
  • 3、预分配一块较大的显存,程序退出时释放显存

为提高效率,我们可以选择第三种,已知我们动态输入最大尺寸为 --maxShapes=x:1x3x1080x1920,因此我们直接根据网络输入输出类型分配显存

    std::vector<void*> bindings(mEngine->getNbBindings());  // 1个输入,1个输出CHECK(cudaMalloc(&bindings[0], sizeof(float) * 1 * 3 * 1280 * 1920)); // n*3*h*wCHECK(cudaMalloc(&bindings[1], sizeof(int) * 1 * 1 * * 1280 * 1920)); // n*1*h*w

当输入尺寸超过我们设置的 maxShapes 时,context->setBindingDimensions()将报异常提示,这一种情况就不应该继续执行分配显存,属于输出错误。

2.4、完整代码

#include "opencv2\opencv.hpp"#include "NvInfer.h"
#include <cuda_runtime_api.h>
#include <random>//using namespace nvinfer1;
//using samplesCommon::SampleUniquePtr;#include <fstream>
#include <string>#define CHECK(status)                                                                \do                                                                               \{                                                                                \auto ret = (status);                                                         \if (ret != 0)                                                                \{                                                                            \std::cerr << "Cuda failure: " << ret << std::endl;                       \abort();                                                                 \}                                                                            \} while (0)class Logger : public nvinfer1::ILogger
{
public:Logger(Severity severity = Severity::kWARNING) : severity_(severity) {}virtual void log(Severity severity, const char* msg) noexcept override {// suppress info-level messagesif(severity <= severity_)std::cout << msg << std::endl;}nvinfer1::ILogger& getTRTLogger() noexcept{return *this;}
private:Severity severity_;
};struct InferDeleter
{template <typename T>void operator()(T* obj) const{delete obj;}
};template <typename T>
using SampleUniquePtr = std::unique_ptr<T, InferDeleter>;int inference();int main(int argc, char** argv)
{return inference();
}int inference()
{Logger logger(nvinfer1::ILogger::Severity::kVERBOSE);/*trtexec.exe --onnx=inference_model\model.onnx --explicitBatch --fp16 --minShapes=x:1x3x540x960 --optShapes=x:1x3x720x1280 --maxShapes=x:1x3x1080x1920 --saveEngine=model.trt*/std::string trtFile = R"(C:\Users\wanggao\Desktop\123\inference_model_0\model.trt)";//std::string trtFile = "model.test.trt";std::ifstream ifs(trtFile, std::ifstream::binary);if(!ifs) {return false;}ifs.seekg(0, std::ios_base::end);int size = ifs.tellg();ifs.seekg(0, std::ios_base::beg);std::unique_ptr<char> pData(new char[size]);ifs.read(pData.get(), size);ifs.close();// engine模型//SampleUniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(logger.getTRTLogger())};//auto mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(//    runtime->deserializeCudaEngine(pData.get(), size), InferDeleter());//auto context = SampleUniquePtr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());std::shared_ptr<nvinfer1::ICudaEngine> mEngine;{SampleUniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(logger.getTRTLogger())};mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(pData.get(), size), InferDeleter());}auto context = SampleUniquePtr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());// 输入//cv::Mat img = cv::imread("dog.jpg");cv::Mat img = cv::imread(R"(C:\Users\wanggao\Desktop\123\data\test\t.jpg)");cv::Mat blob = cv::dnn::blobFromImage(img, 1 / 255., img.size(), {0,0,0}, true, false);//blob = blob * 2 - 1;cv::Mat pred(img.size(), CV_32SC1, {255,255,255});//cv::reduceArgMax() //4.8.0context->setBindingDimensions(0, nvinfer1::Dims4{1, 3 , img.rows, img.cols});// 分配显存std::vector<void*> bindings(mEngine->getNbBindings()); //auto type1 = mEngine->getBindingDataType(0);  // kFLOAT  float//auto type2 = mEngine->getBindingDataType(1);  // kINT32  intCHECK(cudaMalloc(&bindings[0], sizeof(float) * 1 * 3 * img.rows * img.cols*3));    // n*3*h*wCHECK(cudaMalloc(&bindings[1], sizeof(int) * 1 * 1 * img.rows * img.cols*3));   // n*1*h*w// 推理// warmingup ...CHECK(cudaMemcpy(bindings[0], static_cast<const void*>(blob.data), 1 * 3 * 640 * 640 * sizeof(float), cudaMemcpyHostToDevice));context->executeV2(bindings.data());context->executeV2(bindings.data());context->executeV2(bindings.data());context->executeV2(bindings.data());CHECK(cudaMemcpy(static_cast<void*>(pred.data), bindings[1], 1 * 84 * 8400 * sizeof(int), cudaMemcpyDeviceToHost));auto t1 = cv::getTickCount();//buffers.copyInputToDevice();CHECK(cudaMemcpy(bindings[0], static_cast<const void*>(blob.data), 3 * img.rows * img.cols * sizeof(float), cudaMemcpyHostToDevice));context->executeV2(bindings.data());// buffers.copyOutputToHost()CHECK(cudaMemcpy(static_cast<void*>(pred.data), bindings[1], pred.total() * sizeof(int), cudaMemcpyDeviceToHost));auto t2 = cv::getTickCount();std::cout << (t2-t1) / cv::getTickFrequency() << std::endl;// 资源释放cudaFree(bindings[0]);cudaFree(bindings[1]);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/611418.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【现代密码学】笔记3.4-3.7--构造安全加密方案、CPA安全、CCA安全 《introduction to modern cryphtography》

【现代密码学】笔记3.4-3.7--构造安全加密方案、CPA安全、CCA安全 《introduction to modern cryphtography》 写在最前面私钥加密与伪随机性 第二部分流加密与CPA多重加密 CPA安全加密方案CPA安全实验、预言机访问&#xff08;oracle access&#xff09; 操作模式伪随机函数PR…

Java微服务系列之 ShardingSphere - ShardingSphere-JDBC

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 系列专栏目录 [Java项…

报错解决:No module named ‘pytorch_lightning‘ 安装pytorch_lightning

报错记录 执行如下代码&#xff1a; import pytorch_lightning报错&#xff1a; No module named ‘pytorch_lightning’ 解决方式 安装pytorch_lightning包即可。 一般情况下&#xff0c;缺失的包通过pip安装&#xff0c;即&#xff1a; pip install pytorch_lightning然…

1 快速前端开发

1 前端开发 目的&#xff1a;开发一个平台&#xff08;网站&#xff09;- 前端开发&#xff1a;HTML、CSS、JavaScript- Web框架&#xff1a;接收请求并处理- MySQL数据库&#xff1a;存储数据地方快速上手&#xff1a;基于Flask Web框架让你快速搭建一个网站出来。1.快速开发…

HarmonyOS应用开发学习笔记 应用上下文Context 获取文件夹路径

1、 HarmoryOS Ability页面的生命周期 2、 Component自定义组件 3、HarmonyOS 应用开发学习笔记 ets组件生命周期 4、HarmonyOS 应用开发学习笔记 ets组件样式定义 Styles装饰器&#xff1a;定义组件重用样式 Extend装饰器&#xff1a;定义扩展组件样式 5、HarmonyOS 应用开发…

14-股票K线图功能-个股日K线SQL分析__ev

需求&#xff1a;统计个股日K线数据&#xff0c;也就是把某只股票每天的最高价&#xff0c;开盘价&#xff0c;收盘价&#xff0c;最低价形成K线图。

山西电力市场日前价格预测【2024-01-11】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2024-01-11&#xff09;山西电力市场全天平均日前电价为231.43元/MWh。其中&#xff0c;最高日前电价为422.21元/MWh&#xff0c;预计出现在18:00。最低日前电价为0.00元/MWh&#xff0c;预计出…

现代软件测试中的自动化测试工具

自动化测试的重要性和优势 引言&#xff1a;随着软件开发的不断发展&#xff0c;自动化测试工具在现代软件测试中扮演着重要角色。提高效率&#xff1a;自动化测试可以加快测试流程&#xff0c;减少人工测试所需的时间和资源。提升准确性&#xff1a;自动化测试工具可以减少人…

PACS医学影像报告管理系统源码带CT三维后处理技术

PACS从各种医学影像检查设备中获取、存储、处理影像数据&#xff0c;传输到体检信息系统中&#xff0c;生成图文并茂的体检报告&#xff0c;满足体检中心高水准、高效率影像处理的需要。 自主知识产权&#xff1a;拥有完整知识产权&#xff0c;能够同其他模块无缝对接 国际标准…

Linux CentOS 7.6安装JDK详细保姆级教程

一、检查系统是否自带jdk java --version 如果有的话&#xff0c;找到对应的文件删除 第一步&#xff1a;先查看Linux自带的JDK有几个&#xff0c;用命令&#xff1a; rpm -qa | grep -i java第二步:删除JDK&#xff0c;执行命令&#xff1a; rpm -qa | grep -i java | xarg…

企业的 Android 移动设备管理 (MDM) 解决方案

移动设备管理可帮助您在不影响最终用户体验的情况下&#xff0c;通过无线方式管理和保护组织的移动设备群&#xff0c;现代 MDM 解决方案还可以控制 App、内容和安全性&#xff0c;因此员工可以毫无顾虑地在托管设备上工作。移动设备管理软件可有效管理个人设备上的公司空间。M…

优化CentOS 7.6的HTTP隧道代理网络性能

在CentOS 7.6上&#xff0c;通过HTTP隧道代理优化网络性能是一项复杂且细致的任务。首先&#xff0c;我们要了解HTTP隧道代理的工作原理&#xff1a;通过建立一个安全的隧道&#xff0c;HTTP隧道代理允许用户绕过某些网络限制&#xff0c;提高数据传输的速度和安全性。然而&…

工业交换机在智慧水务和水处理中的应用

智慧水务是一种基于互联网和物联网技术的水务管理模式。它利用现代信息技术&#xff0c;将传统的水务管理模式升级&#xff0c;实现智慧化的水务管理方式。智慧水务的实现离不开各种先进的技术手段。物联网技术是智慧水务的重要组成部分。通过在水务系统中部署工业交换机、传感…

C/C++调用matlab

C/C调用matlab matlab虽然可以生成C/C的程序&#xff0c;但其能力很有限&#xff0c;很多操作无法生成C/C程序&#xff0c;比如函数求解、优化、拟合等。为了解决这个问题&#xff0c;可以采用matlab和C/C联合编程的方式进行。使用matlab将关键操作打包成dll环境&#xff0c;再…

MySQL 存储引擎全攻略:选择最适合你的数据库引擎

1. MySQL的支持的存储引擎有哪些 官方文档给出的有以下几种&#xff1a; 我们也可以通过SHOW ENGINES命令来查看&#xff1a; 还可以通过ENGINES表查看 2. 存储引擎比较 我们通过存储引擎表来看各自的优点&#xff1a; InnoDB 默认的存储引擎&#xff08;SUPPORT字段为D…

广东做“人工心脏”可以报销啦

&#xff08;人民日报健康客户端记者 杨林宋&#xff09;1月5日&#xff0c;据南方医科大学珠江医院消息&#xff0c;医院为一位57岁患者处于心衰终末期的患者&#xff0c;植入一款国产“人工心脏”——左心室辅助装置。据了解&#xff0c;这是该款“人工心脏”纳入广东省医保准…

py的循环语句(for和while)

前言&#xff1a;本章节和友友们探讨一下py的循环语句&#xff0c;主播觉得稍微有点难主要是太浑了&#xff0c;但是会尽量描述清楚&#xff0c;OK上车&#xff01;&#xff08;本章节有节目效果&#xff09; 目录 一.while循环的基本使用 1.1关于while循环 1.2举例 1.31-1…

[C#]使用winform部署PP-MattingV2人像分割onnx模型

【官方框架地址】 https://github.com/PaddlePaddle/PaddleSeg 【算法介绍】 PP-MattingV2是一种先进的图像和视频抠图算法&#xff0c;由百度公司基于PaddlePaddle深度学习框架开发。它旨在提供更精准和高效的图像分割功能&#xff0c;特别是在处理图像中的细微部分&#xf…

【Copilot使用】

Copilot是什么 copilot有多火&#xff0c;1月4日&#xff0c;科技巨头微软在官网上宣布将为Windows 11 PC推出Copilot键。 Copilot是微软在Windows 11中加入的AI助手&#xff0c;该AI助手是一个集成了在操作系统中的侧边栏工具&#xff0c;可以帮助用户完成各种任务。 Copilo…

C语言之三子棋小游戏的应用

文章目录 前言一、前期准备模块化设计 二、框架搭建三、游戏实现打印棋盘代码优化玩家下棋电脑下棋判断输赢 四、结束 前言 三子棋是一种民间传统游戏&#xff0c;又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战&#xff0c;双方依次在9宫格棋盘上摆放棋子&#…