基于 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
相关文章:
基于 RK3588 的 YOLO 多线程推理多级硬件加速引擎框架设计(代码框架和实现细节)
一、前言 接续上一篇文章,这个部分主要分析代码框架的实现细节和设计理念。 基于RK3588的YOLO多线程推理多级硬件加速引擎框架设计(项目总览和加速效果)-CSDN博客https://blog.csdn.net/plmm__/article/details/146542002?spm1001.2014.300…...
stm32 can 遥控帧的问题
STM32单片机使用CAN协议进行通信 引用这个博客的一段话 CAN的遥控帧(Remote Frame)的主要作用是请求其他节点发送具 有特定ID的数据帧。具体来说,当一个节点需要从另一个节点获取数 据时,它可以发送一个遥控帧,而不是…...
机器人基础知识-1
1.六轴机器人中的六轴是什么? 第一轴(J1):底座旋转 控制机器人整体绕垂直轴旋转(左右摆动),决定工作范围的水平方向。 第二轴(J2):下臂前后摆动 驱动机器人的…...
JAVA- 锁机制介绍 进程锁
进程锁 基于文件的锁基于Socket的锁数据库锁分布式锁基于Redis的分布式锁基于ZooKeeper的分布式锁 实际工作中都是集群部署,通过负载均衡多台服务器工作,所以存在多个进程并发执行情况,而在每台服务器中又存在多个线程并发的情况,…...
如何在WordPress中强制用户使用强密码?
在如今网络安全备受关注的环境下,弱密码问题不容忽视。很多用户习惯在多个网站使用相同且简单的密码,这样一来,若不强制他们在 WordPress 网站上使用强密码,网站的安全性就会受到威胁。尤其对于在线商店、会员网站、多作者博客等站…...
鸿蒙NEXT开发Base64工具类(ArkTs)
import util from ohos.util;/*** Base64 工具类* author: 鸿蒙布道师* since: 2025/03/31*/ export class Base64Util {/*** 创建 Base64Helper 实例* returns Base64Helper 实例*/private static createBase64Helper(): util.Base64Helper {return new util.Base64Helper();}…...
基于HUTOOL实现RSA工具类
一、前言:用 Hutool 简化 RSA 加密开发,提升代码安全与效率 在当今数据安全至关重要的时代,RSA 非对称加密作为保护敏感信息的核心技术,广泛应用于通信加密、数字签名、密钥交换等场景。然而,手动实现 RSA 算法涉及复…...
flink 分组窗口聚合 与 窗口表值函数聚合 的区别
警告:分组窗口聚合已经过时。推荐使用更加强大和有效的窗口表值函数聚合。 参考官方文档 在 Apache Flink 中,分组窗口聚合(Group Window Aggregation) 和 窗口表值函数聚合(Windowing TVF Aggregation)…...
prompt_status:5: command not found: wc解决办法
问题出现背景 想配置uniapp的命令行,在.zprofile配置路径的时候PATH 前面少打了一个$,执行了 source,导致各种命令都失效。 解决办法 用fider 打开用户文件夹,Command Shift .显示隐藏文件,用文本编辑器修改一下&…...
《STL 六大组件之容器篇:简单了解 list》
目录 一、list 简介二、list 的常用接口1. 构造函数(constructor )2. 迭代器(iterator)3. 容量、修改和访问(capacity 、modify and access) 一、list 简介 简单来说,list 就是数据结构初阶中学…...
向量数据库学习笔记(2) —— pgvector 用法 与 最佳实践
关于向量的基础概念,可以参考:向量数据库学习笔记(1) —— 基础概念-CSDN博客 一、 pgvector简介 pgvector 是一款开源的、基于pg的、向量相似性搜索 插件,将您的向量数据与其他数据统一存储在pg中。支持功能包括&…...
TCP的连接建立
面向连接 定义:在发送数据之前,需要建立一条点到点的连接 (参数协商的过程。因为tcp要保证可靠,所以tcp通信是发生在双方之间、两端之间的,两端在正式发送数据之前需要约定一些初始参数,这个过程就是面向连…...
如何让AI帮你做用户运营:用户消费偏好分层和洞察
随着deepseek的爆火,我一直在想能不能用AI来帮我做用户运营,目前deepseek只能提供框架层面的运营建议,还无法实现将订单数据给到它,能够自动化分析并将用户分层,并给出可视化的运营洞察报表。但是,我要告诉…...
二分答案-P8647 [蓝桥杯 2017 省 AB] 分巧克力
题解:P8647 [蓝桥杯 2017 省 AB] 分巧克力 题目传送门 题目链接 一、题目描述 小明有N块不同尺寸的巧克力,需要切出K块相同大小的正方形巧克力分给小朋友们。要求找到能满足条件的最大的正方形边长。 二、题目分析 我们需要从N块巧克力中切出K个相…...
搜广推校招面经六十一
美团推荐算法 一、ANN算法了解么?说几种你了解的ANN算法 ANN 近似最近邻搜索(Approximate Nearest Neighbor Search)算法 1.1. KD-Tree(K-Dimensional Tree,K 维树) 类型: 空间划分数据结构适用场景: 低…...
某地老旧房屋自动化监测项目
1. 项目简介 自从上个世纪90年代以来,我国经济发展迅猛,在此期间大量建筑平地而起,并且多为砖混结构的住房,使用寿命通常约为30-50年,钢筋混凝土结构,钢结构等高层建筑,这些建筑在一般情况下的…...
【第一节】Python爬虫基础-HTTP基本原理
目录 前言 一、URI和URL是什么 二、什么是超文本 三、HTTP和HTTPS的区别 四、HTTP请求过程 五、请求 六、响应 前言 在着手开发爬虫程序之前,我们需要先掌握一些基础概念。本节将详细讲解HTTP的基本工作原理,重点分析从浏览器输入网址到获取网页内…...
docker打包使用有头模式playwright
1.打包镜像 创建Dockerfile文件如下 # playywright 官方镜像 FROM mcr.microsoft.com/playwright:v1.37.0-jammy# 设置非交互式环境变量和时区 ENV DEBIAN_FRONTENDnoninteractive ENV TZEtc/UTC# 安装 Python 3.9 和 pip(修复时区阻塞问题) RUN apt-g…...
VuePress 和 Docusaurus的对比
VuePress 和 Docusaurus 是两个流行的现代静态网站生成器 vuepress:首页 | VuePress Docusaurus:Docusaurus 博客 | Docusaurus中文文档 | Docusaurus中文网 一、技术栈和设计理念 VuePress 技术栈:基于Vue.js,专为技术文档设计,…...
JAVA数据库增删改查
格式 Main.java(测试类) package com.example;import com.example.dao.UserDao; import com.example.model.User;public class Main {public static void main(String[] args) {UserDao userDao new UserDao();// 测试添加用户System.out.println(" 添加用户 ");Us…...
MSTP多域生成树
协议信息 MSTP 兼容 STP 和 RSTP,既可以快速收敛,又提供了数据转发的多个冗余路径,在数据转发过程中实现 VLAN 数据的负载均衡。 MSTP 可以将一个或多个 VLAN 映射到一个 Instance(实例)(一个或多个 VLAN…...
HashMap 在 JDK 1.7 和 JDK 1.8 有什么区别
HashMap 在 JDK 1.7 和 JDK 1.8 中的实现存在显著差异,主要体现在以下几个方面: 1. 数据结构的变化 • JDK 1.7:HashMap 的底层数据结构是数组 单向链表。当哈希冲突发生时,新的元素会插入到链表的头部(头插法&#…...
Mysql忽略大小写
🚀欢迎来到我的【Mysql】专栏🚀 🙋我是小蜗,一名在职牛马。🐒我的博客主页 ➡️ ➡️ 小蜗向前冲的主页🙏🙏欢迎大家的关注,你们的关注是我创作的最大动力🙏🙏在 MySQL 中取消大小写区分主要涉及以下两个层面的配置,具体操作如下: 一、表名大…...
基于TradingView和CTPBee的自动化期货交易系统实现
引言 在量化交易领域,TradingView因其强大的技术分析工具和丰富的指标库而广受欢迎,但是其不支持国内期货自动化交易,CTPBee则是一个优秀的国产Python期货交易接口。本文将介绍如何将两者结合,实现一个完整的自动化交易系统。 本…...
昇腾CANN算子共建仓CANN-Ops正式上线Gitee,首批算子已合入
在人工智能技术呈指数级发展的今天,AI创新已走向更底层的算法创新,以DeepSeek为例,通过MoE模型架构和底层算法创新,不仅获取极佳的模型性能,又更大程度释放硬件性能,降低硬件使用成本。 算子,作…...
基于PyQt5的自动化任务管理软件:高效、智能的任务调度与执行管理
基于PyQt5的自动化任务管理软件:高效、智能的任务调度与执行管理 相关资源文件已经打包成EXE文件,可双击直接运行程序,且文章末尾已附上相关源码,以供大家学习交流,博主主页还有更多Python相关程序案例,秉着…...
Pycharm(八):字符串切片
一、字符串分片介绍 对操作的对象截取其中一部分的操作,比如想要获取字符串“888666qq.com前面的qq号的时候就可以用切片。 字符串、列表、元组都支持切片操作。 语法:字符串变量名 [起始:结束:步长] 口诀:切片其实很简单,只顾头来…...
C++编程学习笔记:函数相关特性、引用与编译流程
目录 一、函数的缺省参数 (一)全缺省参数 (二)半缺省参数 二、函数重载 (一)参数类型不同 (二)参数个数不同 (三)参数类型顺序不同 三、引用相关问题…...
Nginx 配置 HTTPS 与 WSS 完整指南
Nginx 配置 HTTPS 与 WSS 完整指南 本教程将手把手教你如何为网站配置 HTTPS 加密访问,并通过反向代理实现安全的 WebSocket(WSS)通信。以 https://www.zhegepai.cn 域名为例,完整流程约需 30 分钟完成。 一、前置准备 1.1 域名…...
链表基本操作
文章目录 1、单链表1.1 链表的创建1.2 链表的遍历1.3 链表的删除1.4 链表的插入1.5 链表和数组 2、双向链表2.1 双链表的创建2.2 双链表的删除2.3 双链表的插入2.4 双向循环链表2.5 双链表优缺点 1、单链表 链表是一种物理存储单元上非连续、非顺序的存储结构,插入…...
【huggingface 数据下载】ssh / https 不同的下载流程,hf 镜像下载注意事项
ssh 下载流程 在 linux 服务器上生成 ssh key将 pub key 放入 huggingface 的 setting 中通过 git lfs install 然后 git clone githf.co … 来下载数据 遇到的问题 一直卡在 Updating files 后 卡住的可能原因: 系统当前限制了允许监视的最大文件数࿱…...
简单版CentOS7配置haproxy
一、实验步骤 1、自行下载pes的tar包 然后解压到家目录下 tar -xzvf pes.tar.gz 2、创建一个目录 mkdir docker-compose-pes-lb2 3、在这个目录下写两个文件docker-compose.yml和haproxy.cfg docker-compose.yml version: 3 services: db: image: mysql:5.7.44 container…...
leetcode146.LRU缓存
思路源自 【面试高频】146. LRU 缓存 采用哈希表双向链表 put一个键值对时,采用头插法将缓存块置于等级较高的位置,如果put数量超出限制,那么就将尾部的缓存块删除,以此达到置换的一个效果 get一个键值对也是同样的思路…...
SpringIoC和DI
文章目录 OCP开闭原则DIP(依赖倒置原则)IOC(控制反转)依赖注入DI基于XML配置Beanset注入构造注入 使用注解存储beanController方法注解Bean扫描路径依赖注入三种注入方式优缺点分析 引入 当我们写了一个程序,遵循SpringMVC三层架构,表现层调用业务逻辑层…...
vue 路由
目录 一、路由的使用 二、声明式导航 2.1 声明式导航 2.2 声明式导航路由传参 2.2.1.字符串写法 2.2.2.对象写法 2.2.3 query 传参和 param 传参总结 2.3 命名路由 2.4 可选操作符 2.5 props 参数 三、编程式导航 3.1 replace 和 push 跳转…...
JAVA常见的 JVM 参数及其典型默认值
在 Java 线上应用中,JVM 参数的默认值取决于具体的 JVM 实现(如 Oracle JDK、OpenJDK、Zulu 等)、版本(如 Java 8、11、17 等)以及运行环境(物理机、容器等)。以下是常见的 JVM 参数及其典型默认…...
文件压缩与解压(zip4j)
maven依赖 <dependency><groupId>net.lingala.zip4j</groupId><artifactId>zip4j</artifactId><version>2.11.5</version></dependency>示例 //参数配置ZipParameters parameters new ZipParameters();parameters.setCompres…...
【操作系统】查内存泄漏方法
【操作系统】查内存泄漏方法 1. 通用检测方法1.1 代码审查1.2 运行时监测 2.Linux平台检测工具2.1 Valgrind工具套件2.2 AddressSanitizer (ASan)2.3 mtrace 3.Windows平台检测工具3.1 Visual Studio诊断工具3.2 CRT调试堆 4.嵌入式系统检测方法4.1 RT-Thread内存检测4.2 自定义…...
oracle常用sql
获取主键 1. 查询主键的两种常用方法 Oracle 的主键信息存储在以下两个视图中: USER_CONSTRAINTS:存储当前用户下所有表的约束信息(如主键、外键等)。 USER_CONS_COLUMNS:存储约束对应的列信息。 方法 1ÿ…...
【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】【思路篇】A题解题全流程(持续更新)
【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】A题解题全流程-思路(持续更新) 写在前面: 1、A题、C题将会持续更新,陆续更新发布文章 2、赛题交流咨询Q群:1037590285 3、全家桶依旧包含: 代码、…...
Qt 信号量使用方法
Qt 信号量使用方法 QSemaphore 类 常用函数介绍 函数名称函数功能QSemaphore()构造并初始化对象acquire()尝试获取n个资源,如果没有那么多资源,线程将阻塞直到有n个资源可用available()返回当前信号量可用的资源个数,这个数永远不可能为负…...
C++进阶——封装哈希表实现unordered_map/set
与红黑树封装map/set基本相似,只是unordered_map/set是单向迭代器,模板多传一个HashFunc。 目录 1、源码及框架分析 2、模拟实现unordered_map/set 2.1 复用的哈希表框架及Insert 2.2 iterator的实现 2.2.1 iteartor的核心源码 2.2.2 iterator的实…...
AI Agent 实战:搭建个人在线旅游助手
AI Agent 实战:搭建个人在线旅游助手 本次实验中,我们将继续探索 Agent 的提示词,学习更加规范的提示词撰写方法。 本实验中你将掌握的知识点 使用 Dify 构建 Agent 的方法结构化的提示词撰写技巧变量的使用方法 1. 准备 在新建 Agent 之…...
CSS中的overflow属性
在 CSS 中,overflow 属性用于控制当一个元素的内容溢出其指定的区域时,应该如何处理溢出的部分。通常用于盒模型(如 div)中,指定内容超出容器时的显示方式。 overflow 属性的常用值: 1. visible(…...
【Unity】处理文字显示不全的问题
1.选中字体文件,检查 MultiAtlasTeextures 是否勾选,未勾选的话,先勾选保存后查看是否显示正常 2.勾选后未正常显示,则在搜索框中输入未显示的文本,确认字体图集是否包含该文本,然后点击Update Atlas Textu…...
蓝桥备赛指南(11):递归简介
递归的介绍 概念:递归是指函数直接或间接调用自身的过程。 解释递归的两个关键要素: 基本情况(递归终止条件):递归函数中的一个条件,当满足该条件时,递归终止,避免无限递归。可以…...
Python 图片水印处理工具
自定义水印文本自定义水印位置支持图片裁剪支持各种图片格式 from PIL import Image, ImageDraw, ImageFont import osclass ImageWatermarker:def __init__(self, font_pathNone, font_size40):"""初始化水印处理器font_path: 字体文件路径,默认使…...
从零开始:如何打造一套完整的UI设计系统?
1. 建立色彩系统 色彩系统是设计系统的基础之一,它不仅影响界面的整体美感,还对用户体验有着深远的影响。首先,设计师需要定义主色调、辅助色和强调色,并确保这些颜色在不同场景下的应用保持一致。使用工具如Adobe Color或Coolor…...
Jenkins + CICD流程一键自动部署Vue前端项目(保姆级)
git仓库地址:参考以下代码完成,或者采用自己的代码。 南泽/cicd-test 拉取项目代码到本地 使用云服务器或虚拟机采用docker部署jenkins 安装docker过程省略 采用docker部署jenkins,注意这里的命令,一定要映射docker路径,否则无…...
c# 虚函数、接口、抽象区别和应用场景
文章目录 定义和语法实现要求继承和使用场景总结访问修饰符设计目的性能扩展性在 C# 里,虚函数、接口和抽象函数都能助力实现多态性,不过它们的定义、使用场景和特点存在差异,下面为你详细剖析: 定义和语法 虚函数:虚函数在基类里定义,使用 virtual 关键字,且有默认的实…...