基于 RK3588 的 YOLO 多线程推理多级硬件加速引擎框架设计(代码框架和实现细节)

一、前言

        接续上一篇文章,这个部分主要分析代码框架的实现细节和设计理念。

基于RK3588的YOLO多线程推理多级硬件加速引擎框架设计(项目总览和加速效果)-CSDN博客https://blog.csdn.net/plmm__/article/details/146542002?spm=1001.2014.3001.5501

二、框架分析

        在原作者的基础上,我增加了命令行的参数解析、多态视频读取引擎、硬件视频解码、RGA 硬件图像缩放,色彩空间转换,以及部分代码优化和内存管理调整。

1、命令行参数解析

        使用 ConfigParser 类封装,便于移植:

头文件 parse_config.hpp:

#ifndef _PARSE_CONFIG_HPP_
#define _PARSE_CONFIG_HPP_#include <iostream>
#include <string>
#include <SharedTypes.hpp>/* 定义配置解析类 */
class ConfigParser {public:// 输入格式int input_format;  // 显示帮助信息void print_help(const std::string &program_name) const;// 打印配置信息void printConfig(const AppConfig &config) const;// 解析命令行参数AppConfig parse_arguments(int argc, char *argv[]) const;private:// 私有成员(如果有需要可以添加)};#endif

        这里的 AppConfig 是参数列表结构体,定义在全项目的共享头文件 Shared_Types.hpp 中:

/* 定义命令行参数结构体 */ 
struct AppConfig {// 在屏幕显示 FPSbool screen_fps = false;// 在终端打印 FPSbool print_fps = false;// 是否使用openclbool opencl = true;// 是否打印命令行参数bool verbose = false;// 视频加载引擎,默认为 ffmpegint read_engine = READ_ENGINE::EN_FFMPEG;// 输入格式,默认为视频int input_format = INPUT_FORMAT::IN_VIDEO;// 硬件加速,默认为 RGAint accels_2d = ACCELS_2D::ACC_RGA;// 线程数,默认为1int threads = 1;// rknn 模型路径string model_path = "";// 输入源    string input = "";// 解码器,默认为 h264_rkmppstring decodec = "h264_rkmpp";
};

源文件较大,这里仅放一个长短命令解析的部分截图:

        各位可根据自己喜好,修改参数列表,我比较喜欢设置默认值,直接执行可执行文件时,只需要传递必要的参数。

2、多态视频读取引擎

        原作者使用 OpenCV 进行视频读取和取帧操作,为了保留 OpenCV 的读取,我使用多态的方式可以灵活选择 OpenCV 和 FFmpeg 两种方式进行读取。本节均只介绍头文件中的接口,具体实现较长,还请读者移步 Github 。整体框架为:

(1)​Reader(基类)​

定义了视频读取操作的通用接口(如 open、close、readFrame 等)。

作为所有具体读取器(如 FFmpegReader、OpencvReader 等)的基类,利用多态性实现运行时动态选择具体的实现类。

#ifndef READER_H
#define READER_H#include <string>
#include "opencv2/core.hpp"/*** @Description: 基类引擎* @return {*}*/
class Reader {
public:// 析构虚函数virtual ~Reader() = default;/* 纯虚函数接口 */virtual void openVideo(const std::string& filePath) = 0;virtual bool readFrame(cv::Mat& frame) = 0;virtual void closeVideo() = 0;
};#endif // READER_H

(2)​FFmpegReader 或 OpencvReader(Reader 的子类)​

继承自 Reader 基类。

实现了基类中定义的虚函数,具体使用 FFmpeg 或 OpenCV 库提供的函数来处理视频操作。

在初始化时,可能配置和加载与读取器相关的资源或参数。

#ifndef FFMPEGREADER_H
#define FFMPEGREADER_H#include <iostream>
#include "Reader.hpp"
#include "preprocess.h"
#include "SharedTypes.hpp"#include <opencv2/opencv.hpp>
extern "C" {
#include <libavutil/frame.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}/*** @Description: FFmpeg 引擎* @return {*}*/
class FFmpegReader : public Reader {
public:FFmpegReader(const string& decodec, const int& accels_2d);~FFmpegReader() override;void openVideo(const std::string& filePath) override;bool readFrame(cv::Mat& frame) override;void closeVideo() override;// 获取视频信息void print_video_info(const string& filePath);int getWidth() const;int getHeight() const;AVRational getTimeBase() const;double getFrameRate() const;private:string decodec;                             // 解码器int accels_2d;                              // 2D 硬件加速类型AVFormatContext *formatContext = nullptr;   // 输入文件的上下文AVCodecContext *codecContext = nullptr;     // 解码器上下文const AVCodec* codec = nullptr;             // 解码器int videoStreamIndex = -1;                  // 视频流的索引AVStream *video_stream;                     // 视频流AVFrame *tempFrame = nullptr;               // 临时帧(用于解码)AVPacket *packet = nullptr;                 // 数据包int NV12_to_BGR(cv::Mat& bgr_frame);int FFmpeg_yuv420sp_to_bgr(cv::Mat& bgr_frame);void AV_Frame_To_CVMat(cv::Mat& nv12_mat);
};#endif // FFMPEGREADER_H
#ifndef OPENCVREADER_H
#define OPENCVREADER_H#include "Reader.hpp"
#include <iostream>
#include <opencv2/opencv.hpp>/*** @Description: Opencv 引擎* @return {*}*/
class OpencvReader : public Reader {
public:OpencvReader();~OpencvReader() override;void openVideo(const std::string& filePath) override;bool readFrame(cv::Mat& frame) override;void closeVideo() override;private:cv::VideoCapture videoCapture; // OpenCV 视频捕获对象
};#endif // OPENCVREADER_H

(3)VideoReader(中间件)​

提供给 main 函数或其他上层模块使用的接口。

负责根据配置或输入动态选择并实例化合适的 Reader 子类(如 FFmpegReader 或 OpencvReader)。

封装了对具体 Reader 实例的管理,简化了上层模块对视频读取操作的调用。

#ifndef VIDEOREADER_H
#define VIDEOREADER_H#include <memory>
#include <string>#include "SharedTypes.hpp"
#include "Reader.hpp"/*** @Description: 视频读取器* @return {*}*/
class VideoReader {
public:VideoReader(const AppConfig& config);~VideoReader();/* 以下禁止拷贝和允许移动两部分实现:1、提高性能;2、管理独占资源;3、现代C++鼓励使用移动语义和智能指针等工具来管理资源。 */// 禁止拷贝构造和拷贝赋值VideoReader(const VideoReader&) = delete;VideoReader& operator=(const VideoReader&) = delete;// 允许移动构造和移动赋值VideoReader(VideoReader&&) = default;VideoReader& operator=(VideoReader&&) = default;/* 函数接口 */bool readFrame(cv::Mat &frame);  // 读取一帧void Close_Video();              // 关闭视频private:// 使用智能指针管理资源,这里只是声明, ​没有申请内存std::unique_ptr<Reader> reader_ptr; // 加载引擎void Init_Load_Engine(const int& engine, const string& decodec, const int& accels_2d);
};#endif // VIDEOREADER_H

(4)​main 函数

使用 VideoReader 提供的统一接口来操作视频,无需关心底层使用了哪种具体的读取器实现。

创建 VideoReader:

读取帧:

3、硬件视频解码

        这部分主要由 FFmpeg 实现,通过 FFmpeg 来调用 Rkmpp 解码器。这里需要注意,FFmpeg 不是官方源码,而是 rockchip 版本的 ffmpeg-rockchip,来自 nyanmisaka 大佬的项目:

nyanmisaka/ffmpeg-rockchip: FFmpeg with async and zero-copy Rockchip MPP & RGA supporthttps://github.com/nyanmisaka/ffmpeg-rockchip        专门针对瑞芯微的 Rockchip MPP & RGA 进行适配和优化,可以在编译时开启 rkmpp 解码支持和 RGA 过滤器支持。编译方法移步:

编译支持 RKmpp 和 RGA 的 ffmpeg 源码_ffmpeg支持mpp-CSDN博客https://blog.csdn.net/plmm__/article/details/146188927?spm=1001.2014.3001.5501        代码部分就是常规的 FFmpeg 进行视频解码,我这里分为了两部分:打开视频文件和读取视频帧。

打开视频文件

/*** @Description: 打开视频文件* @param {string} &filePath: * @return {*}*/
void FFmpegReader::openVideo(const std::string& filePath) {/* 分配一个 AVFormatContext */formatContext = avformat_alloc_context();if (!formatContext)throw std::runtime_error("Couldn't allocate format context");/* 打开视频文件 */// 并读取头部信息,此时编解码器尚未开启if (avformat_open_input(&formatContext, filePath.c_str(), nullptr, nullptr) != 0)throw std::runtime_error("Couldn't open video file");/* 读取媒体文件的数据包以获取流信息 */if (avformat_find_stream_info(formatContext, nullptr) < 0)throw std::runtime_error("Couldn't find stream information");/* 查找视频流 AVMEDIA_TYPE_VIDEO */// -1, -1,意味着没有额外的选择条件,返回值是流索引videoStreamIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (videoStreamIndex < 0)throw std::runtime_error("Couldn't find a video stream");/* 查找解码器 */codec = avcodec_find_decoder_by_name(this->decodec.c_str());if (!codec)throw std::runtime_error("Decoder not found");/* 初始化编解码器上下文 */ codecContext = avcodec_alloc_context3(codec);if (!codecContext)throw std::runtime_error("Couldn't allocate decoder context");/* 获取视频流,它包含了视频流的元数据和参数 */video_stream = formatContext->streams[videoStreamIndex];/* 复制视频参数到解码器上下文 */ if (avcodec_parameters_to_context(codecContext, video_stream->codecpar) < 0)throw std::runtime_error("Couldn't copy decoder context");/* 自动选择线程数 */codecContext->thread_count = 0;/* 打开编解码器 */ if (avcodec_open2(codecContext, codec, nullptr) < 0)throw std::runtime_error("Couldn't open decoder");/* 分配 AVPacket 和 AVFrame */ tempFrame = av_frame_alloc();packet = av_packet_alloc();if (!tempFrame || !packet)throw std::runtime_error("Couldn't allocate frame or packet"); 
}

        其中下面的代码需要注意:

/* 自动选择线程数 */codecContext->thread_count = 0;

这个变量主要用于设置 FFmpeg 工作线程数量,0 代表自动选择,具体的实验可以看这篇文章:

解决 FFmpeg 使用 C/C++ 接口时,解码没有 shell 快的问题(使用多线程)-CSDN博客https://blog.csdn.net/plmm__/article/details/146523965?spm=1001.2014.3001.5501

读取视频帧

/*** @Description: 读取一帧* @param {Mat&} frame: 取出的帧* @return {*}*/
bool FFmpegReader::readFrame(cv::Mat& frame) {// 读取帧/*if (av_read_frame(formatContext, packet) < 0) {return false; // 没有更多帧}*/while (av_read_frame(formatContext, packet) >= 0) {if (packet->stream_index != videoStreamIndex) {av_packet_unref(packet);continue;}break;}// 如果是视频流if (packet->stream_index != videoStreamIndex) {cerr << "Not a video stream: " << packet->stream_index << " != " << videoStreamIndex << endl;av_packet_unref(packet);return false; // 不是视频流}// 发送数据包到解码器if (avcodec_send_packet(codecContext, packet) < 0) {std::cerr << "Failed to send packet to decoder" << std::endl;av_packet_unref(packet);return false; // 发送数据包失败}// 接收解码后的帧if (avcodec_receive_frame(codecContext, tempFrame) < 0) {std::cerr << "Failed to receive frame from decoder" << std::endl;av_packet_unref(packet);return false;}// 成功读取一帧,保存在 tempFrame 中// 将帧数据转换为 cv::Mat BGR 格式if (this->NV12_to_BGR(frame) != 0) {std::cerr << "Failed to convert YUV420SP to BGR" << std::endl;av_packet_unref(packet);return false;}// 释放数据包av_packet_unref(packet);return true; // 处理完成
}

        av_read_frame 函数在实测过程中发现开头几帧取出后不是视频流,因此直接使用 while 跳过。在成功取出帧后,会保存在 tempFrame 中,为 AVFrame 格式,色彩空间为 NV12,由解码器决定,我使用 h264_rkmpp 解码器,默认输出是 NV12。

4、RGA 硬件加速

        目前主要有三个地方使用到了图像的缩放和格式转换的操作,并且三个操作是前后关系,分别是上一节取出视频帧后要将 NV12 转为 BGR888,转为 YOLO 输入的 RGB888,以及输入尺寸的修改。

NV12 转为 BGR888

        由于需要保持接口的通用性,与 OpenCV 取帧保持一致(OpenCV 解码后为 BGR888 格式), 并且数据传输使用 OpenCV 的 cv::Mat 对象进行图像传输,所以在取出帧后进行了颜色空间的转换,并改用 cv::Mat 进行保存:

/*** @Description: 转换格式,NV12 转 BGR*               该函数内有三种转换方式:*                  1. FFmpeg SwsContext 软件转换  *                  2. OpenCV 软件转换,可启用 opencl(目前区别不大)*                  3. RGA 硬件加速转换* @param {Mat&} frame: * @return {*}*/
int FFmpegReader::NV12_to_BGR(cv::Mat& bgr_frame) {if (tempFrame->format != AV_PIX_FMT_NV12) {return -EXIT_FAILURE; // 格式错误}// 设置输出帧的尺寸和格式,防止地址无法访问bgr_frame.create(tempFrame->height, tempFrame->width, CV_8UC3);#if 0 // 方式1:使用 FFmpeg SwsContext 软件转换return this->FFmpeg_yuv420sp_to_bgr(bgr_frame);
#endif// 创建一个完整的 NV12 数据块(Y + UV 交错)cv::Mat nv12_mat(tempFrame->height + tempFrame->height / 2, tempFrame->width, CV_8UC1);// 将 AVFrame 内的数据,转换为 OpenCV Mat 格式保存this->AV_Frame_To_CVMat(nv12_mat);// 硬件加速if (this->accels_2d == ACCELS_2D::ACC_OPENCV) {// 方式2:使用 OpenCV 软件转换cv::cvtColor(nv12_mat, bgr_frame, cv::COLOR_YUV2BGR_NV12);return EXIT_SUCCESS;} else if (this->accels_2d == ACCELS_2D::ACC_RGA) {// 方式3:使用 RGA 硬件加速转换return RGA_yuv420sp_to_bgr((uint8_t *)nv12_mat.data, tempFrame->width, tempFrame->height, bgr_frame);}elsereturn -EXIT_FAILURE;
}

        这个函数可以使用三种方式进行转换,分别是:

1. FFmpeg SwsContext 软件转换

2. OpenCV 软件转换

3. RGA 硬件转换

        三种转换方式的源码较多,可在项目源码中查看。根据目前实测的结果(只针对当前转换函数),SwsContext 转换一次耗时约 20ms,RGA 约 2-5ms,OpenCV 约 2-4ms。RGA 转换接口可能和我的接口调用方式有关,还有优化的空间,平均值甚至不如 OpenCV。

转为 YOLO 输入的 RGB888

        这里的转换操作是放在了推理线程中,理论上是在多线程进行:

// YOLO 推理需要 RGB 格式,后处理需要 BGR 格式// 即使前处理时提前转换为 RGB,后处理部分任然需要转换为 BGR,需要在本函数中保留两种格式if (this->config.accels_2d == ACCELS_2D::ACC_OPENCV) {cv::cvtColor(orig_img, rgb_img, cv::COLOR_BGR2RGB);}else if (this->config.accels_2d == ACCELS_2D::ACC_RGA) {if (RGA_bgr_to_rgb(orig_img, rgb_img) != 0) {cout << "RGA_bgr_to_rgb error" << endl;return cv::Mat();}}else {cout << "Unsupported 2D acceleration" << endl;return cv::Mat();}

        在原作者转换逻辑的基础上,我增加了 OpenCV 和 RGA 的选择。注释中也说明了为什么需要 BGR 转 RGB 这一步,这也和 cv::Mat 对象的默认格式有关,cv::imshow 显示时也是需要数据为 BGR,与 YOLO 的输入格式相反。

输入尺寸的修改

        即输入图像的 resize:

// 图像缩放if (orig_img.cols != width || orig_img.rows != height){// 如果需要缩放,再对 resized_img 申请大小,节约内存开销resized_img.create(height, width, CV_8UC3);if (this->config.accels_2d == ACCELS_2D::ACC_OPENCV){// 打包模型输入尺寸cv::Size target_size(width, height);float min_scale = std::min(scale_w, scale_h);scale_w = min_scale;scale_h = min_scale;letterbox(rgb_img, resized_img, pads, min_scale, target_size, this->config.opencl);}else if (this->config.accels_2d == ACCELS_2D::ACC_RGA){ret = RGA_resize(rgb_img, resized_img);if (ret != 0) {cout << "resize_rga error" << endl;}}else {cout << "Unsupported 2D acceleration" << endl;return cv::Mat();}inputs[0].buf = resized_img.data;}else{inputs[0].buf = rgb_img.data;}

        上面与瑞芯微官方的 YOLO demo 是一样的,我对 letterbox 函数内部做了 OpenCL 的一个修改:

void letterbox_with_opencl(const cv::Mat &image, cv::UMat &padded_image, BOX_RECT &pads, const float scale, const cv::Size &target_size, const cv::Scalar &pad_color) {// 将输入图像转换为 UMatcv::UMat uImage = image.getUMat(cv::ACCESS_READ);// 调整图像大小cv::UMat resized_image;cv::resize(uImage, resized_image, cv::Size(), scale, scale);if (uImage.empty()) {std::cerr << "Error: uImage is empty." << std::endl;return;}if (resized_image.empty()) {std::cerr << "Error: resized_image is empty." << std::endl;return;}// 计算填充大小int pad_width = target_size.width - resized_image.cols;int pad_height = target_size.height - resized_image.rows;pads.left = pad_width / 2;pads.right = pad_width - pads.left;pads.top = pad_height / 2;pads.bottom = pad_height - pads.top;// 在图像周围添加填充cv::copyMakeBorder(resized_image, padded_image, pads.top, pads.bottom, pads.left, pads.right, cv::BORDER_CONSTANT, pad_color);
}/*** @Description: OpenCV 图像预处理* @return {*}*/
void letterbox(const cv::Mat &image, cv::Mat &padded_image, BOX_RECT &pads, const float scale, const cv::Size &target_size, bool Use_opencl, const cv::Scalar &pad_color)
{// 图像数据检查if (image.empty()) {std::cerr << "Error: Input image is empty." << std::endl;return;}// 调整图像大小cv::Mat resized_image;if (Use_opencl){// 预处理图像cv::UMat U_padded_image;letterbox_with_opencl(image, U_padded_image, pads, scale, target_size, pad_color);// 将处理后的图像从 GPU 内存复制回 CPU 内存(如果需要显示)// padded_image = U_padded_image.getMat(cv::ACCESS_READ);// padded_image = std::move(U_padded_image.getMat(cv::ACCESS_READ));padded_image = U_padded_image.getMat(cv::ACCESS_READ).clone(); // 深拷贝return ;}cv::resize(image, resized_image, cv::Size(), scale, scale);// 计算填充大小int pad_width = target_size.width - resized_image.cols;int pad_height = target_size.height - resized_image.rows;pads.left = pad_width / 2;pads.right = pad_width - pads.left;pads.top = pad_height / 2;pads.bottom = pad_height - pads.top;// 在图像周围添加填充cv::copyMakeBorder(resized_image, padded_image, pads.top, pads.bottom, pads.left, pads.right, cv::BORDER_CONSTANT, pad_color);
}

        使用 cv::UMat 对象来调用 OpenCL 进行 resize 的并行计算。

5、其他

还有一些 C 语言的接口,我封装为了类的形式,虽然牺牲了一些性能,不过为了项目的通用性和可维护性,很多都使用 C++ 的语法替换掉了,比如加载模型的函数:

原始的 C 函数:

static unsigned char *load_data(FILE *fp, size_t ofst, size_t sz)
{unsigned char *data;int ret;data = NULL;if (NULL == fp){return NULL;}ret = fseek(fp, ofst, SEEK_SET);if (ret != 0){printf("blob seek failure.\n");return NULL;}data = (unsigned char *)malloc(sz);if (data == NULL){printf("buffer malloc failure.\n");return NULL;}ret = fread(data, 1, sz, fp);return data;
}static unsigned char *load_model(const char *filename, int *model_size)
{FILE *fp;unsigned char *data;fp = fopen(filename, "rb");if (NULL == fp){printf("Open file %s failed.\n", filename);return NULL;}fseek(fp, 0, SEEK_END);int size = ftell(fp);data = load_data(fp, 0, size);fclose(fp);*model_size = size;return data;
}

改用更便捷的方式,并且内存的申请放到了函数外,由调用者进行管理,提高内存维护的便捷性:

/*** @Description: 获取文件大小* @param {string&} filename: * @return {size_t}: 返回字节数,失败返回0*/
static size_t get_file_size(const std::string& filename) {// std::ios::ate:打开文件后立即将文件指针移动到文件末尾(at end)std::ifstream ifs(filename, std::ios::binary | std::ios::ate);if (!ifs.is_open())return 0;// 通过文件尾定位获取大小size_t size = ifs.tellg();ifs.close();return size;
}/*** @Description: 加载文件数据* @param {ifstream&} ifs: * @param {size_t} offset: * @param {unsigned char*} buffer: * @param {size_t} size: * @return {*}*/
static bool load_data(std::ifstream& ifs, size_t offset, unsigned char* buffer, size_t size) {if (!ifs.is_open()) {std::cerr << "File stream not open" << std::endl;return false;}// 定位到指定位置ifs.seekg(offset, std::ios::beg);if (ifs.fail()) {std::cerr << "Seek failed at offset " << offset << std::endl;return false;}ifs.read(reinterpret_cast<char*>(buffer), size);// ifs.gcount():返回实际读取的字节数if (ifs.gcount() != static_cast<std::streamsize>(size)) {std::cerr << "Read failed, expected " << size << " bytes, got " << ifs.gcount() << std::endl;return false;}return true;
}/*** @Description: 加载模型* @param {string} &filename: * @param {unsigned char} *buffer: * @param {size_t&} buffer_size: * @return {*}*/
static bool load_model(const std::string &filename, unsigned char *buffer, const size_t& buffer_size) {// std::ios::binary:以二进制模式打开文件std::ifstream ifs(filename, std::ios::binary);if (!ifs){std::cerr << "Failed to open: " << filename << std::endl;return false;}if (buffer_size == 0){std::cerr << "Failed to open: " << filename << std::endl;return false;}return load_data(ifs, 0, buffer, buffer_size);
}

三、总结

        以上就是我做的一些修改的粗略描述,具体细节我也都在代码中做了注释。希望这个项目可以帮到有需要硬件解码,以及正在学习 RGA 接口的小伙伴。各位读者有任何修改意见,欢迎与我联系,代码会放至Gitee 和 Github,我有空也会持续完善优化:

Gitee:

YOLO_RKNN_Acceleration_Program: YOLO multi-threaded and hardware-accelerated inference framework based on RKNNhttps://gitee.com/lrf1125962926/yolo_rknn_acceleration_programGithub:

1125962926/YOLO_RKNN_Acceleration_Program: YOLO multi-threaded and hardware-accelerated inference framework based on RKNNhttps://github.com/1125962926/YOLO_RKNN_Acceleration_Program

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

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

相关文章

LeetCode Hot100 刷题笔记(7)—— 贪心

目录 前言 一、贪心 1. 买卖股票的最佳时机 2. 跳跃游戏 3. 跳跃游戏 II 4. 划分字母区间 前言 一、贪心&#xff1a;买卖股票的最佳时机&#xff0c;跳跃游戏&#xff0c;跳跃游戏 II&#xff0c;划分字母区间。 一、贪心 1. 买卖股票的最佳时机 原题链接&#xff1a;121. …

SQL语句的训练

DELECT FROM 蜀国 WHEHE name 刘玄德 AND 创业进度<0.5 AND 存活状态 true&#xff1b; 基础的sql语句 SELECT >选择列FROM >确认数据源JOIN >联合操作WHERE >筛选数据GROUP BY >分组 HAVING >过滤分组的数据DISTINCT >去重ORDEY BY > 排序…

汽车 HMI 设计的发展趋势与设计要点

一、汽车HMI设计的发展历程与现状 汽车人机交互界面&#xff08;HMI&#xff09;设计经历了从简单到复杂、从单一到多元的演变过程。2012年以前&#xff0c;汽车HMI主要依赖物理按键进行操作&#xff0c;交互方式较为单一。随着特斯拉Model S的推出&#xff0c;触控屏逐渐成为…

基于51单片机的模拟条形码识别系统proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1AtAry19X3BgavLqXcM4scg 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C51 是一款常用的 8 位单片机&#xff0c;由 Atmel 公司&#xff08;现已被 Microchip 收…

CD22.【C++ Dev】类和对象(13) 流提取运算符的重载和const成员

目录 1.流提取运算符>>的重载 知识回顾 重载方法 operator<<格式 operator>>格式 使用cin对日期类对象写入数据 如果想指定格式输入 方法1:getchar() 方法2:使用临时变量接收字符 完善operator>>代码(修bug) 2.类中的权限问题(const成员) …

Spring 核心技术解析【纯干货版】- XIX:Spring 日志模块 Spring-Jcl 模块精讲

在现代 Java 开发中&#xff0c;日志是调试、监控和维护应用程序的重要工具。Spring 作为企业级框架&#xff0c;提供了 Spring-Jcl 作为日志抽象层&#xff0c;使开发者可以灵活切换不同的日志实现&#xff0c;而无需修改业务代码。本篇文章将深入解析 Spring-Jcl 模块&#x…

Hadoop集群---运维管理和技巧

一. daemon 守护进程管理 1. NameNode守护进程管理 hadoop-daemon.sh start namenode 2. DataNode守护进程管理 hadoop-daemon.sh start datanode 3. ResourceManager守护进程管理 yarn-daemon.sh start resourcemanager 4. NodeManager守护进程管理 yarn-daemon.sh st…

ngx_log_init

定义在 src\core\ngx_log.c ngx_log_t * ngx_log_init(u_char *prefix, u_char *error_log) {u_char *p, *name;size_t nlen, plen;ngx_log.file &ngx_log_file;ngx_log.log_level NGX_LOG_NOTICE;if (error_log NULL) {error_log (u_char *) NGX_ERROR_LOG_PATH;}…

网络华为HCIA+HCIP 策略路由,双点双向

目录 路由策略&#xff0c;策略路由 策略路由优势 策略路由分类 接口策略路由 双点双向 双点双向路由引入特点: 联系 路由回灌和环路问题 路由策略&#xff0c;策略路由 路由策略:是对路由条目进行控制&#xff0c;通过控制路由条目影响报文的转发路径&#xff0c;即路…

水下成像机理分析

一般情况下, 水下环境泛指浸入到人工水体 (如水库、人工湖等)或自然水体(如海洋、河流、湖 泊、含水层等)中的区域。在水下环境中所拍摄 的图像由于普遍受到光照、波长、水中悬浮颗粒物 等因素的影响&#xff0c;导致生成的水下图像出现模糊、退 化、偏色等现象&#xff0c;图像…

MySQL的数据目录以及日志

1.MySQL数据目录 MySQL服务器的管理信息、业务数据、⽇志⽂件、磁盘缓冲⽂件默认存储在数据⽬录下.数据目录保存了我们用户的信息,以及我们创建的数据库和表的数据.维护了日志文件等.mysqld主要操作的就是我们的数据目录. 如何查看数据目录: ll /var/lib/mysql#ll 是查看指令 …

论文阅读:Dual Anchor Graph Fuzzy Clustering for Multiview Data

论文地址:Dual Anchor Graph Fuzzy Clustering for Multiview Data | IEEE Journals & Magazine | IEEE Xplore 代码地址&#xff1a;https://github.com/BBKing49/DAG_FC 摘要 多视角锚图聚类近年来成为一个重要的研究领域&#xff0c;催生了多个高效的方法。然而&#…

32f4,串口1,usart.c.h2025

usart.c #include "sys.h" #include "usart.h" #include "led.h" // #include "stdlib.h" #include "stdarg.h" #include "stdio.h" //加入以下代码,支持printf函数,而不需要选择use MicroLIB #if 1#pragma…

C语言:一组位操作宏

解析协议时&#xff0c;取得位域的值是一种常见操作&#xff0c;这些宏可以辅助我们工作。 /* ** 将x的第n位置1 ** ** x 0x00000000 ** BIT_SET(x, 7) 0x00000080 */ #define BIT_SET(x, n) ((x) | (1 << (n)))/* ** 将x的第n位置为0 ** ** x 0x00000080 ** …

记一个使用BigDecimal所有类型变为整数的问题

场景 通过 Excel 导入数据&#xff0c;数据中包含金额。数据库类型 decimal(18, 6) 问题 Excel 导入后所有的金额列都被四舍五入。经过测试&#xff0c;只有数据有整数时所有数据才会被四舍五入&#xff0c;全部为浮点类型没有问题。 解决 强制设置小数位数 // RoundingM…

nodejs、socket.io、express + 实时线上聊天系统(自用笔记)

留个链接给自己参考用&#xff1a; socket.io官方文档&#xff1a;介绍 | Socket.IO nodejs基础语法&#xff1a;大前端技能讲解&#xff1a;NodeJS、Npm、Es6、Webpack_nodejs webpack-CSDN博客 socket.io教学&#xff1a;半小时学会socket.io【中英字幕】Learn Socket.Io …

配置网络编辑器

网络断开的原因 1.由于网络未连接的情况 解决方法 方法1&#xff1a;检查网卡配置 cd /etc/syscongfig/network_scripts vi ifcfg_ens31 方法2&#xff1a;打开虚拟机编辑--- 虚拟网络编辑器 查看ip地址是否在可用的网段范围内 修改后重启网络 systemctl restart netwo…

vscode代码片段的设置与使用

在 Visual Studio Code (VS Code) 中&#xff0c;可以通过自定义**代码片段&#xff08;Snippets&#xff09;**快速插入常用代码模板。以下是详细设置步骤&#xff1a; 步骤 1&#xff1a;打开代码片段设置 按下快捷键 Ctrl Shift P&#xff08;Windows/Linux&#xff09;或…

基于S函数的simulink仿真

基于S函数的simulink仿真 S函数可以用计算机语言来描述动态系统。在控制系统设计中&#xff0c;S函数可以用来描述控制算法、自适应算法和模型动力学方程。 S函数中使用文本方式输入公式和方程&#xff0c;适合复杂动态系统的数学描述&#xff0c;并且在仿真过程中可以对仿真…

做题记录:和为K的子数组

来自leetcode 560 前言 自己只会暴力&#xff0c;这里就是记录一下前缀和哈希表的做法&#xff0c;来自灵神的前缀和哈希表&#xff1a;从两次遍历到一次遍历&#xff0c;附变形题 正文 首先&#xff0c;这道题无法使用滑动窗口&#xff0c;因为滑动窗口需要满足单调性&am…