基于 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.pswp.cn/web/74418.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/74418.shtml
英文地址,請注明出處:http://en.pswp.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…