RK3588部署YOLOv8(2):OpenCV和RGA实现模型前处理对比

目录

前言

1. 结果对比

1.1 时间对比

1.2 CPU和NPU占用对比

2. RGA实现YOLO前处理

2.1 实现思路

2.2 处理类的声明

2.3 处理类的实现

总结


前言

RK平台上有RGA (Raster Graphic Acceleration Unit) 加速,使用RGA可以减少资源占用、加速图片处理速度。因此,在部署YOLOv8是针对RGA和OpenCV的分别进行了实现,并对性能、速度和资源占用进行对比。


1. 结果对比

1.1 时间对比

总共跑100次计算平均时间。

纯OpenCV实现:

OPencv实现resize+pad
[Convert] Step1: Check input pointer => 0 us
[Convert] Step2: define intermediate Mat => 37 us
[Convert] Step3: cv::resize => 9564 us
[Convert] Step4: create pad_img => 1629 us
[Convert] Step5: compute position => 61 us
[Convert] Step6: copyTo => 340 us
[Convert] Step7: return => 54 us
INFO: image resize time 12.15 ms
INFO: total infer time 22.71 ms: model time is 22.45 ms and postprocess time is 0.26ms
Iteration 100 - time: 35.034000 ms
Total execution time: 3770.930000 ms
Average execution time: 37.709300 ms

纯RGA实现:

RGA实现
[Convert] Step1: Check input pointer => 1 us
[Convert] Step2: Set format/bpp => 92 us
[Convert] Step3: Calculate buffer sizes => 13 us
[Convert] Step4: Define variables => 12 us
[Convert] Step5: Compute border => 13 us
[Convert] Step6: Alloc & memcpy src => 10048 us
[Convert] Step7: Alloc resized buffer => 477 us
[Convert] Step8: Alloc dst buffer => 269 us
[Convert] Step9: importbuffer_fd => 3494 us
[Convert] Step10: wrapbuffer_handle => 80 us
[Convert] Step11: imresize => 2714 us
[Convert] Step12: immakeBorder => 1154 us
[Convert] Step13: copy result => 428 us
[Convert] Step14: cleanup => 2607 us
INFO: image resize time 24.26 ms
INFO: total infer time 22.10 ms: model time is 21.84 ms and postprocess time is 0.26ms
Iteration 100 - time: 46.496000 ms
Total execution time: 4398.143000 ms
Average execution time: 43.981430 ms

总结:

(1)从上可以看到OpenCV最占时间的是resize步骤,需要9~10ms,而RGA只要2~3ms。

(2)但使用RGA,如果图片数据不在DMA缓冲区,则需要进行拷贝,导致耗时太久。

(3)最终,导致RGA实现resize要比OpenCV慢了6~7ms。

1.2 CPU和NPU占用对比

跑单个模型持续推理。

纯OpenCV实现:

CPU占用率:120%~140%

NPU占用率:43%~48%

纯RGA实现:

CPU占用率:50%~60%

NPU占用率:35%~40%

总结:

(1)OPenCV使用CPU多线程计算差值,导致CPU占用率较高

(2)RGA在DMA缓冲区使用硬件计算,减少对CPU依赖。

(3)RGA比OpenCV减少了60%的CPU占用和10%的NPU占用。


2. RGA实现YOLO前处理

参考代码:

[1] https://github.com/airockchip/librga

[2] rga_resize_demo.cpp

[3] rga_padding_demo.cpp

2.1 实现思路

        在DMA缓冲区,每有一个形状,便需要一个CPU指针、句柄和缓冲区,导致分配内存、处理起来极其麻烦,且宽度需要16倍对齐(高度没有要求)、同时还存在着4G范围寻址问题。所以,实际应用要起来还是很麻烦的(尤其是自定义的任意尺寸输入)。

        所以,这里仅使用RGA对合规的图片做resize,再使用OpenCV做pad填充。

        由于工资预算有限,这里仅实现摄像头输入的原图尺寸(宽度满足16倍长,主要以1920×1080为主)的图片的前处理。

补充(实现任意尺寸处理的思路):

(1) 设传入的宽和高为:[H,W],目标宽高(模型输入大小)为:[H_T, W_T],INT_UP表示向上取整函数。
(2) 原16倍宽度 W_16 为: INT_UP(W / 16) * 16,则现将原图先填充(仅右边填充)到尺寸[H, W_16]。
(3) 计算放缩比 R = min(H / H_T, W_16 / W_t)。
(4) 继续计算得到目标放缩尺寸宽度的16倍 W_T_16 为:INT_UP(W_T_16 / 16) * 16。
(5) 然后更新放缩比 R = min(H / H_T, W_T_16 / R / W_T),此过程可能需要迭代。
(6) 得到放缩比 R 后,计算原图真正要填充到的尺寸为:[H_T / R, W_T_16 / R]。
(7) 然后对原图仅做右边和下面的填充,这样就把原图对齐到16倍长(放缩前后均是)。

2.2 处理类的声明

#include <iostream>
#include <memory>
#include <numeric>
#include <vector>
#include <algorithm>
#include "opencv2/opencv.hpp"
#include "common.h"
// 增加RGA库实现pad resize
#include <rga/RgaApi.h>
#include <rga/im2d.hpp>
#include <rga/rga.h>
#include <rga/RgaUtils.h>
#include <dma/dma_alloc.h>
// 打印时间
#include <sys/time.h>
#include <stdint.h>
#include <stdio.h>// RGA 版本的 Pad Resize 处理类
class ImagePreProcessRGA {public:// 构造函数:输入图像为 width x height,目标尺寸为正方形 target_size x target_sizeImagePreProcessRGA(int width, int height, int target_size);// 构造函数:输入图像为 width x height,目标尺寸为 target_width x target_heightImagePreProcessRGA(int width, int height, int target_width, int target_height);// 对输入图像数据进行pad resize处理,返回处理后的图像数据(unique_ptr管理)std::unique_ptr<uint8_t[]> Convert(const det_model_input& input);// 获取letterbox信息const letterbox_t &get_letter_box() { return letterbox_; }private:double scale_;  // 缩放比例int input_width_, input_height_;            // 输入图像尺寸int real_input_width_, real_input_height_;  // 输入的实际尺寸int target_width_, target_height_;          // 目标图像尺寸int new_width_, new_height_;                // 缩放后图像的尺寸(填输入的尺寸缩放后,经过填充才能变成目标尺寸)int padding_x_, padding_y_;                 // pad的总尺寸(左右、上下)letterbox_t letterbox_;     // letterbox信息记录缩放比例及左右/上下填充(一般为居中填充)
};

2.3 处理类的实现

        实现RGB的3通道或者4通道的图片。

// 构造函数1:只传一个target_size,默认目标是正方形
ImagePreProcessRGA::ImagePreProcessRGA(int width, int height, int target_size): input_width_(width), input_height_(height), target_width_(target_size), target_height_(target_size){// ------------------【Step1:根据最大边计算放缩比例】---------------------// 如果原图是 (width x height),目标是 (target_size x target_size),则 scale = target_size / max(width, height)scale_ = static_cast<double>(target_size) / std::max(input_width_, input_height_);// ------------------【Step2:计算缩放后尺寸】----------------------------new_width_  = static_cast<int>(input_width_  * scale_);new_height_ = static_cast<int>(input_height_ * scale_);// ------------------【Step3:计算在目标图像中的剩余填充】------------------padding_x_ = target_size - new_width_;padding_y_ = target_size - new_height_;// ------------------【Step4:更新 letterbox】--------------------------letterbox_.scale = scale_;letterbox_.x_pad = padding_x_ / 2;letterbox_.y_pad = padding_y_ / 2;// ------------------【可选:打印结果】-----------------------------------// printf(">>> After => new_width_=%d, new_height_=%d, scale=%.3f\n", new_width_, new_height_, scale_);// printf(">>> padding_x_=%d, padding_y_=%d\n", padding_x_, padding_y_);
}// 构造函数2:传入独立的target_width和target_height,可能目标不是正方形
ImagePreProcessRGA::ImagePreProcessRGA(int width, int height, int target_width, int target_height): input_width_(width), input_height_(height), target_width_(target_width), target_height_(target_height){// ------------------【Step1:分别计算宽高缩放比例】-----------------------double width_scale  = static_cast<double>(target_width_)  / input_width_;double height_scale = static_cast<double>(target_height_) / input_height_;// 取较小的缩放比例scale_ = std::min(width_scale, height_scale);// ------------------【Step2:计算缩放后尺寸】----------------------------new_width_  = static_cast<int>(input_width_  * scale_);new_height_ = static_cast<int>(input_height_ * scale_);// ------------------【Step3:计算填充大小】------------------------------padding_x_ = target_width_  - new_width_;padding_y_ = target_height_ - new_height_;// ------------------【Step4:更新 letterbox】---------------------------letterbox_.scale = scale_;letterbox_.x_pad = padding_x_ / 2;letterbox_.y_pad = padding_y_ / 2;// ------------------【可选:打印结果】-----------------------------------// printf(">>> After => new_width_=%d, new_height_=%d, scale=%.3f\n", new_width_, new_height_, scale_);// printf(">>> padding_x_=%d, padding_y_=%d\n", padding_x_, padding_y_);
}// 核心函数:基于RGA对输入图像进行pad resize,并返回处理后的图像数据
std::unique_ptr<uint8_t[]> ImagePreProcessRGA::Convert(const det_model_input& input){// -------------------【Step0:在函数开头声明所有变量】-------------------// 中间处理函数返回值int ret = 0;// DMA fdint src_dma_fd     = -1;int resized_dma_fd = -1;// CPU指针uint8_t *src_buf     = nullptr;uint8_t *resized_buf = nullptr;// RGA handlerga_buffer_handle_t src_handle     = 0;rga_buffer_handle_t resized_handle = 0;// RGA bufferrga_buffer_t rga_src;rga_buffer_t rga_resized;memset(&rga_src, 0, sizeof(rga_src));memset(&rga_resized, 0, sizeof(rga_resized));// 最终的返回结果std::unique_ptr<uint8_t[]> final_data;// 其他局部变量int bpp_src    = 0;int bpp_dst    = 3;  // 目标一定3通道int src_format = 0;int dst_format = RK_FORMAT_RGB_888;// 源图大小和resize后大小int src_size     = 0;  int resized_size = 0;  // 用于 pad 的边界int left=0, right=0, top=0, bottom=0;// 常用114作为灰度cv::Scalar pad_color(114,114,114);// -------------------【Step1:基础检查】------------------------------------if (!input.data || input.width <= 0 || input.height <= 0 || (input.channel != 3 && input.channel != 4)){fprintf(stderr, "ERROR: invalid input data or channel.\n");return nullptr;}// 根据通道数决定 bpp & formatbpp_src    = (input.channel == 4) ? 4 : 3;src_format = (input.channel == 4) ? RK_FORMAT_RGBA_8888 : RK_FORMAT_RGB_888;// 源图大小src_size   = input.width  * input.height  * bpp_src;// resize后大小int out_size_w = new_width_;   // 由构造函数算好int out_size_h = new_height_;  // 由构造函数算好resized_size = out_size_w * out_size_h * bpp_dst;std::unique_ptr<uint8_t[]> resized_cpu(new uint8_t[resized_size]);// 用于后面 OpenCV padleft   = padding_x_ / 2;right  = padding_x_ - left;top    = padding_y_ / 2;bottom = padding_y_ - top;// -------------------【Step2:分配 src_buf, resized_buf】-------------------ret = dma_buf_alloc(DMA_HEAP_DMA32_PATH, src_size, &src_dma_fd, (void**)&src_buf);if (ret < 0 || !src_buf) {fprintf(stderr, "ERROR: alloc src_buf failed.\n");return nullptr;}ret = dma_buf_alloc(DMA_HEAP_DMA32_PATH, resized_size, &resized_dma_fd, (void**)&resized_buf);if (ret < 0 || !resized_buf) {fprintf(stderr, "ERROR: alloc resized_buf failed.\n");goto cleanup;}// 不考虑16对齐等,只做最原始的 YOLO思路。只需将 input.data 拷贝到 src_bufmemcpy(src_buf, input.data, src_size);// -------------------【Step3:import & wrap】-------------------------------src_handle     = importbuffer_fd(src_dma_fd,     src_size);resized_handle = importbuffer_fd(resized_dma_fd, resized_size);if (!src_handle || !resized_handle) {fprintf(stderr, "ERROR: importbuffer_fd failed.\n");ret = -1;goto cleanup;}rga_src     = wrapbuffer_handle(src_handle,     input.width,  input.height,  src_format);rga_resized = wrapbuffer_handle(resized_handle, out_size_w,   out_size_h,    dst_format);// -------------------【Step4:RGA仅做 resize or color convert+resize】-------------------if (input.channel == 4) {// RGBA => color convert => resizedIM_STATUS st_cvt = imcvtcolor(rga_src, rga_resized, RK_FORMAT_RGBA_8888, RK_FORMAT_RGB_888);if (st_cvt != IM_STATUS_SUCCESS) {fprintf(stderr, "ERROR: imcvtcolor failed: %s.\n", imStrError(st_cvt));ret = -1;goto cleanup;}}else {// channel=3 => 直接 resizeIM_STATUS st_resize = imresize(rga_src, rga_resized, 0, 0, INTER_LINEAR);if (st_resize != IM_STATUS_SUCCESS) {fprintf(stderr, "ERROR: imresize failed: %s.\n", imStrError(st_resize));ret = -1;goto cleanup;}}// -------------------【Step5:将 resized_buf 拷回 CPU】-------------------// 拿到 resize 后的 RGB 图像数据memcpy(resized_cpu.get(), resized_buf, resized_size);// -------------------【Step6:用OpenCV进行 pad】-------------------{// 1) 构造一个 cv::Mat 指向 resized_cpucv::Mat resized_mat(new_height_, new_width_, CV_8UC3, resized_cpu.get());// 2) 构造一个 pad_mat (target_height_ x target_width_),初始颜色(114,114,114)cv::Mat pad_mat(target_height_, target_width_, CV_8UC3, pad_color);// 3) 计算在 pad_mat 中的放置位置// left=padding_x_/2, top=padding_y_/2cv::Rect roi(left, top, resized_mat.cols, resized_mat.rows);// 4) 拷贝 resized_mat 到 pad_mat 对应区域resized_mat.copyTo(pad_mat(roi));// 5) 将 pad_mat 拷到 final_dataint final_size = target_width_ * target_height_ * 3; // 3通道final_data.reset(new uint8_t[final_size]);memcpy(final_data.get(), pad_mat.data, final_size);}cleanup:// -------------------【Step7:释放资源】-------------------if (src_handle)     releasebuffer_handle(src_handle);if (resized_handle) releasebuffer_handle(resized_handle);if (src_buf)     dma_buf_free(src_size,   &src_dma_fd,     src_buf);if (resized_buf) dma_buf_free(resized_size,&resized_dma_fd, resized_buf);// 若 ret!=0, 返回 nullptrif (ret != 0) {return nullptr;}// 否则返回 final_data,即 "pad后" 的图像return final_data;
}

总结

这样实现资源占用还是约等于纯RGA实现,在推理单张图的时候,速度还可能更快一些:

RGA-resize+OpenCV-pad:
>>> After => new_width_=640, new_height_=360, scale=0.333
>>> padding_x_=0, padding_y_=24
[Convert] Step1: check => 9 usOriginal Input => width=1920, height=1080, channel=3Resize => new_width_=640, new_height_=360Pad => target_width_=640, target_height_=384
[Convert] Step2: alloc => 187 us
[Convert] StepFILL => copy input => 6220800 bytes
[Convert] StepFILL => 1709 us
rga_api version 1.10.1_[0]
[Convert] Step3: wrap => src=(1920x1080), resized=(640x360)
[Convert] Step3 => 892 us
[Convert] Step4: imresize => done
[Convert] Step4 => 2294 us
[Convert] Step5: copy resized => 691200 bytes
[Convert] Step5 => 543 us
[Convert] Step6: OpenCV pad => final_size=737280
[Convert] Step6 => 1314 us
[Convert] Step7: cleanup => 648 us
INFO: image resize time 7.64 ms
INFO: total infer time 28.11 ms: model time is 27.93 ms and postprocess time is 0.18ms
Iteration 1 - time: 35.846000 ms
Total execution time: 35.846000 ms
Average execution time: 35.846000 msOpenCV:
INFO: image resize time 9.49 ms
INFO: total infer time 23.12 ms: model time is 23.09 ms and postprocess time is 0.03ms
Iteration 1 - time: 32.688000 ms

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

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

相关文章

破局者登场:中国首款AI原生IDE Trae深度解析--开启人机协同编程新纪元

摘要 字节跳动于2025年3月3日正式发布中国首款AI原生集成开发环境Trae国内版&#xff0c;以动态协作、全场景AI赋能及本土化适配为核心优势。Trae内置Doubao-1.5-pro与DeepSeek R1/V3双引擎&#xff0c;支持基于自然语言生成端到端代码框架、实时上下文感知与智能Bug修复&…

【PyCharm】Python和PyCharm的相互关系和使用联动介绍

李升伟 整理 Python 是一种广泛使用的编程语言&#xff0c;而 PyCharm 是 JetBrains 开发的专门用于 Python 开发的集成开发环境&#xff08;IDE&#xff09;。以下是它们的相互关系和使用联动的介绍&#xff1a; 1. Python 和 PyCharm 的关系 Python&#xff1a;一种解释型、…

SNIPAR:快速实现亲缘个体的基因型分离与推断

SNIPAR&#xff1a;快速实现亲缘个体的基因型分离与推断 近日&#xff0c;英国剑桥大学研究团队在Nature Genetics上发表了最新研究成果——SNIPAR&#xff08;SNP-based Inference of Pedigree relationship, Ancestry, and Recombination&#xff09;。这一强大的工具可以帮助…

3.11记录

leetcode刷题&#xff1a; 1. 334. 递增的三元子序列 - 力扣&#xff08;LeetCode&#xff09; 方法一&#xff1a;使用贪心算法求解 class Solution(object):def increasingTriplet(self, nums):first nums[0]second float(inf)for i in nums:if i>second:return Truee…

阿里云操作系统控制台评测:国产AI+运维 一站式运维管理平台

阿里云操作系统控制台评测&#xff1a;国产AI运维 一站式运维管理平台 引言 随着云计算技术的飞速发展&#xff0c;企业在云端的运维管理面临更高的要求。阿里云操作系统控制台作为一款集运维管理、智能助手和系统诊断等多功能于一体的工具&#xff0c;正逐步成为企业高效管理…

大语言模型学习--向量数据库Milvus实践

Milvus是目前比较流行的开源向量数据库&#xff0c;其官网地址 Milvus 是什么&#xff1f; | Milvus 文档 1.Milvus简介 Milvus 是一种高性能、高扩展性的向量数据库。Milvus 提供强大的数据建模功能&#xff0c;能够将非结构化或多模式数据组织成结构化的 Collections。它支…

DeepSeek Kimi详细生成PPT的步骤

以下是使用 DeepSeek 和 Kimi 协作生成 PPT 的详细步骤&#xff0c;结合了两者的优势实现高效创作&#xff1a; 第一步&#xff1a;使用 DeepSeek 生成 PPT 大纲或内容 明确需求并输入提示词 在 DeepSeek 的对话界面中&#xff0c;输入具体指令&#xff0c;要求生成 PPT 大纲或…

Visual Studio 安装及使用教程(Windows)【安装】

文章目录 一、 Visual Studio 下载1. 官网下载2. 其它渠道 二、Visual Studio 安装三、Visual Studio 使用四、Visual Studio 其它设置1. 桌面快捷方式2. 更改主题、字体大小 软件 / 环境安装及配置目录 一、 Visual Studio 下载 1. 官网下载 安装地址&#xff1a;https://vi…

Java多线程与高并发专题——阻塞和非阻塞队列的并发安全原理是什么?

引入 之前我们探究了常见的阻塞队列的特点&#xff0c;在本文我们就以 ArrayBlockingQueue 为例&#xff0c;首先分析 BlockingQueue &#xff0c;也就是阻塞队列的线程安全原理&#xff0c;然后再看看它的兄弟——非阻塞队列的并发安全原理。 ArrayBlockingQueue 源码分析 …

关于ngx-datatable no data empty message自定义模板解决方案

背景&#xff1a;由于ngx-dataable插件默认没有数据时显示的文案是no data to display&#xff0c;且没有任何样式。这里希望通过自定义模板来实现。但目前github中有一个案例是通过设置代码&#xff1a; https://swimlane.github.io/ngx-datatable/empty** <ngx-datatable…

Matlab 双线性插值(二维)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 双线性插值是一种 二维插值方法,用于计算 栅格(Grid) 或 像素点 之间的插值值。它主要用于 图像缩放、旋转、变换 等操作,以在新像素位置估算灰度值或颜色值。 如上图所示,假设存在一个二维离散函数(如图像)…

coding ability 展开第二幕(双指针——巩固篇)超详细!!!!

文章目录 前言有效的三角形个数思路 查找总价格为目标值的两个商品思路 两数之和思路 三数之和思路 四数之和思路 总结 前言 本专栏的上篇&#xff0c;讲述了双指针的一些基础的算法习题 今天我们来学习更进一步的双指针用法吧 其实也是大相径庭&#xff0c;和前面的差不多&…

L1-056 猜数字

L1-056 猜数字 - 团体程序设计天梯赛-练习集 (pintia.cn) 题解 这道题要求&#xff1a;一群人坐在一起&#xff0c;每人猜一个 100 以内的数&#xff0c;谁的数字最接近大家平均数的一半就赢。现在需要编写程序来计算&#xff0c;其中需要存入玩家的名字&#xff08;字符串&a…

处理Java中的异常

处理Java中的异常 在 Java 中&#xff0c;异常处理是通过 try-catch-finally 语句来实现的。Java 提供了一种强大的机制&#xff0c;用于捕捉和处理程序运行中的各种错误和异常。通过这种方式&#xff0c;你可以有效地捕捉到可能导致程序崩溃的错误&#xff0c;并做出相应的处…

一维数组的增删改查:对元素的影响

一维数组的增删改查:对元素的影响(C语言) 在C语言中,一维数组是一种存储一组相同类型元素的数据结构。它在内存中是连续存储的,每个元素都可以通过索引来访问和修改。在这篇博文中,我们将详细探讨一维数组的增、删、改、查操作,并分析它们对数组元素的影响。 1. 一维数…

项目实操分享:一个基于 Flask 的音乐生成系统,能够根据用户指定的参数自动生成 MIDI 音乐并转换为音频文件

在线体验音乐创作&#xff1a;AI Music Creator - AI Music Creator 体验者账号密码admin/admin123 系统架构 1.1 核心组件 MusicGenerator 类 负责音乐生成的核心逻辑 包含 MIDI 生成和音频转换功能 管理音乐参数和音轨生成 FluidSynth 集成 用于 MIDI 到音频的转换 …

关于MCP SSE 服务器的工作原理

模型上下文协议&#xff08;Model Context Protocol&#xff0c;简称MCP&#xff09; 是一种全新的开放协议&#xff0c;专门用于标准化地为大语言模型&#xff08;LLMs&#xff09;提供应用场景和数据背景。 你可以把MCP想象成AI领域的“USB-C接口”&#xff0c;它能让不同的A…

计算机:基于深度学习的Web应用安全漏洞检测与扫描

目录 前言 课题背景和意义 实现技术思路 一、算法理论基础 1.1 网络爬虫 1.2 漏洞检测 二、 数据集 三、实验及结果分析 3.1 实验环境搭建 3.2 模型训练 最后 前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,…

win32汇编环境,网络编程入门之二

;运行效果 ;win32汇编环境,网络编程入门之二 ;本教程在前一教程的基础上,研究一下如何得到服务器的返回的信息 ;正常的逻辑是连接上了,然后我发送什么,它返回什么,但是这有一个很尴尬的问题。 ;就是如何表现出来。因为网络可能有延迟,这个延迟并不确定有多久。 ;而程序是顺…

【高分论文密码】AI大模型和R语言的全类型科研图形绘制,从画图、标注、改图、美化、组合、排序分解科研绘图每个步骤

在科研成果竞争日益激烈的当下&#xff0c;「一图胜千言」已成为高水平SCI期刊的硬性门槛——数据显示很多情况的拒稿与图表质量直接相关。科研人员普遍面临的工具效率低、设计规范缺失、多维数据呈现难等痛点&#xff0c;因此科研绘图已成为成果撰写中的至关重要的一个环节&am…