基于Cyclone V SoC利用HLS实现卷积手写体数字识别设计

2023-11-03 01:30

本文主要是介绍基于Cyclone V SoC利用HLS实现卷积手写体数字识别设计,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引文

本文是基于英特尔Cyclone V SoC开发板,利用HLS技术实现三层卷积两层池化两层全连接推理运算的手写体数字识别设计。

硬件环境:

  • Cyclone V  SoC开发板
  • OV5640 摄像头
  • 15.6寸 HDMI显示屏
  • SD卡
  • 电脑

软件环境:

  • Windows 10
  • Quartus prime
  • Eclipse DS-5
  • MobaXterm
  • i++编译环境
  • HLS工具

语言:

  • C
  • Verilog HDL

相关知识理论部分

卷积神经网络 

卷积神经网络(Convolutional Neural Network,CNN)是一种常用的深度学习算法,主要应用于图像处理和计算机视觉领域。CNN在处理图像时,能够自动提取图像中的特征,并将其转化为对应的抽象表示,从而实现图像分类、物体检测、目标识别等任务。

图1.1 卷积神经网络原理

图1.2 CNN基本结构

CNN的基本结构包括卷积层、池化层和全连接层。其中,卷积层是CNN的核心,通过卷积操作对输入图像进行特征提取。卷积层包括多个卷积核,每个卷积核负责检测图像中的一个特定特征,如边缘、纹理等。通过卷积操作,卷积核能够对输入图像进行卷积运算,得到一个新的特征图,从而实现对输入图像的特征提取。

图1.3 卷积层

池化层用于减小特征图的尺寸,从而降低后续层的计算量。通常采用最大池化或平均池化操作,将每个小区域内的最大值或平均值作为输出,从而实现对特征图的下采样操作。

图1.4 池化层

全连接层用于对特征进行分类或回归。它将卷积层和池化层输出的特征向量拉直成一个一维向量,然后通过一个全连接层进行分类或回归操作。

图1.5全连接层

CNN的训练过程通常采用反向传播算法。该算法基于梯度下降优化策略,通过计算预测值和实际值之间的误差,并反向传播误差,更新模型的参数,从而提高模型的精度。

图1.6 卷积神经网络原理

总之,卷积神经网络具有自动提取特征、处理图像和计算机视觉任务等优点,已经成为图像处理和计算机视觉领域的核心技术之一。

全连接神经网络

全连接神经网络是一种最基本的人工神经网络模型,也被称为多层感知机(Multilayer Perceptron,MLP)。它由多层神经元组成,每层神经元与前一层的所有神经元相连,形成完全连接的网络结构。每个神经元接收上一层的所有神经元的输出,并产生自己的输出,同时将输出传递给下一层的所有神经元。这种全连接的结构使得神经网络能够学习输入数据中的复杂关系,并输出相应的结果。

图1.7 神经网络模型

全连接神经网络通常包括输入层、隐藏层和输出层三个部分。其中输入层接受原始的输入数据,隐藏层则通过一系列线性和非线性的变换将输入数据进行特征提取和变换,输出层将变换后的特征映射到对应的目标结果上。

全连接神经网络的训练过程通常使用反向传播算法(Backpropagation,BP)进行,该算法能够有效地调整网络的权重和偏置,从而最小化输出结果与实际结果之间的误差。在训练过程中,通过不断地将输入数据和目标输出数据传递给网络,神经网络能够自适应地调整自身的权重和偏置,从而使得输出结果更加接近目标结果,提高了预测的准确率。

图1.8 反向传播原理

全连接神经网络在许多领域都有广泛的应用,包括图像识别、语音识别、自然语言处理、游戏AI等。同时,由于全连接神经网络的计算量较大,也在一定程度上限制了其在实际应用中的发展。

HLS(高层次综合)

HLS(High-Level Synthesis)这项技术。它是一种自动化的硬件设计工具,可以将高级语言(例如C、C++等)转化为FPGA(Field-Programmable Gate Array)硬件描述语言,从而实现高效的硬件设计。HLS技术可以大大提高硬件设计的效率和可重用性,减少了传统硬件设计所需的时间和成本。同时,HLS技术也可以提供更高的设计灵活性和可扩展性,使硬件设计更加容易。

图2.9 HLS原理

图2.10 HLS原理

在数字图像处理和机器学习领域,HLS技术可以用于加速卷积神经网络(CNN)等复杂算法的计算过程,从而实现更快速和高效的图像识别和处理。HLS技术可以将卷积操作等计算密集型部分移植到FPGA中实现硬件加速,同时保持系统的高准确率。这种软硬件协同设计的方式可以有效地利用FPGA的并行计算能力和高度可编程性,加速数字图像处理和识别的过程。

SoC FPGA 

SoC FPGA(System on Chip Field Programmable Gate Array)是将FPGA和SoC(System on Chip)功能集成在同一芯片上的一种集成电路。它既有FPGA的可编程性和灵活性,又有SoC的集成度和可靠性。

图2.12 SoC FPGA原理

SoC FPGA通常由FPGA硬件逻辑单元、ARM处理器、内存、高速串行接口、外设接口等部分组成。FPGA逻辑单元可以根据应用需求进行灵活的配置和实现,可以实现不同类型的处理和计算;ARM处理器可以提供更高的计算性能和运行多种软件应用程序的能力,同时也可以在处理器和FPGA之间进行数据交换和协同处理,实现软硬件协同设计;高速串行接口和外设接口可以方便地与其他外部设备进行通信和控制。

项目介绍

项目框架以及数据流向

基于SoCFPGA的手写体识别项目,包括FPGA和HPS两大板块。在FPGA板块中,细分为OV5640摄像头、HDMI显示和算法处理三个模块。HPS板块包含DDR3和手写体算法控制两个模块。

数据流向如下:OV5640摄像头模块采集RGB565格式的图像数据,每个像素占两个字节,分辨率为1024x760,通过Avalon接口的memory_map Master接口输出给DDR3高速存储器。在DDR3中,设计了一块frame_buffer数据缓冲区,用于存储采集到的图像数据。同时,DDR3将采集到的数据和相应的控制命令通过Avalon接口的memory_map流接口时序输出给HDMI模块,将RGB888格式的三个字节数据发送到显示屏上,实现实时的图像显示。 另外,DDR3中采集到的OV5640数据也同时传送给算法控制模块,进行手写体识别算法的处理。算法控制模块会对DDR3中的数据进行0填充等预处理操作,并输出处理后的数据和相应的控制命令给FPGA中的ACCSYSTEM数据处理模块。ACCSYSTEM数据处理模块会执行3层卷积、2层池化和2层全连接等操作,将处理后的特征映射传送回DDR3,并通过HDMI模块显示在屏幕上,从而实现手写体识别的正确结果展示。

模块介绍

DDR3模块:作为PS端的高速存储器,负责存储从OV5640摄像头模块采集到的图像数据和从手写体识别算法控制模块输出的数据。DDR3模块通过高速接口与PL端的其他模块进行数据传输。

手写体识别算法控制模块:负责控制PL端的ACCsystem推理算法模块的操作,并向DDR3模块写入图像数据和相应的控制命令。该模块可能包括卷积神经网络(CNN)和全连接神经网络(FCN)等手写体识别算法的实现,并根据输入图像进行推理和分类,生成识别结果。

HDMI的IP模块:负责与HDMI显示屏的连接和通信,将从PL端输出的图像数据转换为HDMI信号,通过HDMI接口输出到显示屏进行实时显示。

图像接收模块:负责从OV5640摄像头模块接收采集到的图像数据,并将其传输到PL端进行后续处理。可能包括图像数据的解析、格式转换等功能。 OV5640摄像头模块:负责采集手写体图像数据,并通过图像接收模块将RGB565格式的图像数据传输到PL端,作为手写体识别的输入。

ACCsystem推理算法模块:作为PL端的数据处理核心,负责接收来自DDR3模块和手写体识别算法控制模块的输入数据,并进行卷积、池化、全连接等推理算法的计算操作。经过处理后,将识别结果传输到DDR3模块和HDMI的IP模块,同时可能还包括与其他模块之间的数据传输和控制信号的处理。 

设计流程

  • 根据PL搭建的摄像头模块、 图像采集处理模块、Avalon协议模块、HDMI图像输入输出模块编写对应的Verilog HDL代码满足功能和时序要求。

  • 根据PS搭建的DDR3高速存储模块、手写体算法控制模块、三层卷积两层池化两层全连接推理算法模块编写对应的C代码满足存储、控制和算法要求。

  • 根据C代码编写的三层卷积两层池化两层全连接推理算法,利用 i++ 环境下的HLS工具转换成Quartus工程并封装成IP。

  • 根据AXI协议和Avalon协议完成PL和PS之间的传输。

项目文件结构

handwriting                                 # 项目名称└──  C5MB_GHRD_Mnist/                    # 黄金工程├── C5MB_top.qpf               # 用于启动quartus的GUI界面├── C5MB_top.v                 # 黄金工程.v设计文件├── hps_0.h                    # 封装IP生成的头文件,用来保持寄存器地址├── soc_system.dtb             # 用于烧入sd卡执行的文件├── app/                       # ps端c代码设计源文件│    ├── MNIST_mb/             # 项目主文件│    │     └── *.c│    └── ...        │        ├── ip/                          # IP目录│    ├── pll/                    # 调用的pll源文件存放目录│    │     └── ...│    ├── hls/                    # hls高层次综合文件│    │     └── ...│    ├── DVP_CAPTURE/            # HDMI输入输出IP文件│    │     └── ...│    └── AccSystem/              # 自己封装的算法源文件存放目录│          └── ...│├── src/                         # PL端的.v源文件│  ├── i2c_master/               # I2C接口时序源文件设计│  │     └── *.v│  ├── *.v│  └── ...      │   ├── output_files/│       ├── soc_system.rbf         # 用于烧入sd卡执行的文件│       └── ... │└── ...

代码示例

全连接神经网络推理算法设计C代码示例

#include <stdio.h>
#include  "HLS/hls.h"
#include  "HLS/stdio.h"
#include "HLS/hls_internal.h"
#include "HLS/math.h"#include"input_0.h"
#include"input_1.h"
#include"input_2.h"
#include"input_3.h"
#include"input_4.h"
#include"input_5.h"
#include"input_6.h"
#include"input_7.h"
#include"input_8.h"
#include"input_9.h"
#include"layer1_bais.h"
#include"layer1_weight.h"
#include"layer2_bais.h"
#include"layer2_weight.h"hls_avalon_slave_component component int handwriting(hls_avalon_slave_memory_argument(784*sizeof(float))float *img,hls_avalon_slave_memory_argument(784*64*sizeof(float))float *w1,hls_avalon_slave_memory_argument(64*sizeof(float))float *b1,hls_avalon_slave_memory_argument(64*10*sizeof(float))float *w2,hls_avalon_slave_memory_argument(10*sizeof(float))float *b2)
{//输入层第一层int    ret1;int    i,j;float   a1[64]={0.0},a2[10]={0.0};for ( i = 0; i < 64; i++)//从什么时候开始跳{for ( j = 0; j < 784; j++)//跳64为拮据的次数{a1[i] += img[j]*w1[j*64+i];}a1[i] += b1[i];//加激活函数a1[i] = (a1[i]>0)? a1[i] : 0 ;}
//第二层for ( i = 0; i < 10; i++)//从什么时候开始跳{for ( j = 0; j < 64; j++)//跳10为拮据的次数{a2[i] += a1[j]*w2[j*10+i];}a2[i] += b2[i];//加激活函数//a2[i] = (a2[i]>0)? a2[i] : 0 ;}//判断输出识别数字float temp=0;for ( i = 0; i < 10; i++){if (a2[i]>temp){temp = a2[i];ret1 = i;}    } return    ret1;
}
int  main()
{int i;float *a3[10]={input_0,input_1,input_2,input_3,input_4,input_5,input_6,input_7,input_8,input_9};for ( i = 0; i < 10; i++){int ret0 = handwriting(a3[i],layer1_weight,layer1_bais,layer2_weight,layer2_bais);printf("input_%d.h检测结果为:%d\n",i,ret0);}return  0;
}

三层卷积两层池化两层全连接推理算法C代码示例

#define _INFER_GLOBLE
#define _PARA_GLOBLE
#include "include.h"
int chrinfer() {//----------------------------------------------//  第一卷积层//----------------------------------------------//将输入图像加载至FPGA的Map存储器中//基地址偏移为0//第一层为1通道输入,6通道输出,输入图像28x28//所以只需要首先加载一次图像数据//然后每次卷积时每个通道再单独加载权重//加载过程中处理Padding,保持输出图像与输入图像大小相同//即上下左右补2圈0PROCESS_TRACE("hls_conv", "loading");int i, j, channel, outchannel, inchannel, ner;//Padding后输入图像大小变为32x32for (i = 0; i < 32; i++) {for (j = 0; j < 32; j++) {//上下补0,即第0,1行与第30,31行,写入Map存储器中的数据全是0if ((i == 0) || (i == 31) || (i == 1) || (i == 30)) {*(acc_data_virtual_base + i * 32 + j) = 0.0;} else {//左右补0,即第0,1列与第30,31列,写入Map存储器中的数据全是0if ((j == 0) || (j == 31) || (j == 1) || (j == 30)) {*(acc_data_virtual_base + i * 32 + j) = 0.0;} else {//根据unit参数把字符图像写入Map存储器中*(acc_data_virtual_base + i * 32 + j) = plate.unit[i - 2][j - 2];}}}}INFO_TRACE("level 1 accconvolution");//第一层开始for (channel = 0; channel < L1CHAN; channel++) {//卷积//加载权重至FPGA Weight存储器中//基地址偏移为FPGA_Weightfor (i = 0; i < ConvSize; i++) {for(j = 0; j < ConvSize; j++){*(acc_data_virtual_base + FPGA_Weight + i * 5 + j) = conv_w1[channel][i][j];}			}//配置卷积加速器//由于输入图像只有一个,因此不累加私有存储器图像(LocalMap)//后续需要偏置处理,因此结果输出到私有存储器中//卷积大小为Padding后的大小,即上下左右补零后的大小,即高宽各加2accconvolution(32, 32, 1, 0);//卷积结果输出到localmap//偏置与激活//加载偏置数据到FPGA的Weight存储器中//基地址偏移为FPGA_Weight*(acc_data_virtual_base + FPGA_Weight) = conv_b1[channel];//配置加速器//由于卷积输出保存在私有存储器中,因此偏置激活的输入数据来自于私有存储器//后续需要池化处理,因此输出到私有存储器//偏置激活的图像大小为卷积后图像大小,与原始输入图像保持相同//执行accbiasrelu(28, 28, 1, 1);//从localmap加载图像,输出到localmap//池化//配置加速器//由于偏置激活结果保存至私有存储器中,因此池化输入来自私有存储器//池化完成后第一层结束,后续无需任何操作,因此输出到FPGA Result存储器中//基地址偏移为FPGA_Result//池化图像大小与偏置激活后图像大小保持相同//执行accpool(28, 28, 1, 0);//从localmap加载图像,输出到result//读取结果//输出图像大小为池化后大小,即高宽各减半(14*14)for (i = 0; i < 14; i++) {for (j = 0; j < 14; j++) {layer1poolout[channel][i][j] = *(acc_data_virtual_base + FPGA_Result + i * 14 + j);}}}//----------------------------------------------//  第二卷积层//----------------------------------------------//与第一层卷积不同的是//第二层卷积有多个输入通道//因此每次独立卷积时都需要单独加载输入图像//L2CHAN个输出特征图INFO_TRACE("level 2 accconvolution");for (outchannel = 0; outchannel < L2CHAN; outchannel++) {//L1CHAN个输入特征图进行卷积并累加for (inchannel = 0; inchannel < L1CHAN; inchannel++) {//卷积//加载特征图//不加Paddingfor (i = 0; i < 14; i++) {for (j = 0; j < 14; j++) {*(acc_data_virtual_base + i * 14 + j) = layer1poolout[inchannel][i][j];			}}//加载权重for (i = 0; i < ConvSize; i++) {for(j = 0; j < ConvSize; j++){*(acc_data_virtual_base + FPGA_Weight + i * 5 + j) = conv_w2[outchannel][inchannel][i][j];}}//配置加速器//输出到私有存储器//每一个输出特征图,需要所有输入特征图分别卷积并累加结果//第一次计算不累加,其余累加//执行if (inchannel == 0){accconvolution(14, 14, 1, 0);}else{accconvolution(14, 14, 1, 1);}}//注意//此处inchannel循环已经结束//已经对所有输入特征图进行卷积并累加//配置加速器//来自私有存储器//输出到私有存储器*(acc_data_virtual_base + FPGA_Weight) = conv_b2[outchannel];//执行accbiasrelu(10, 10, 1, 1);//池化//配置加速器//来自私有存储器//输出到Result//执行accpool(10, 10, 1, 0);//取回池化结果for (i = 0; i < 5; i++) {for (j = 0; j < 5; j++) {layer2poolout[outchannel][i][j] = *(acc_data_virtual_base + FPGA_Result + i * 5 + j);}}}//----------------------------------------------//  第三卷积层//----------------------------------------------//第三卷积层与第二卷积层类似,不加paddingINFO_TRACE("level 3 accconvolution");for (outchannel = 0; outchannel < L3CHAN; outchannel++) {for (inchannel = 0; inchannel < L2CHAN; inchannel++) {for (i = 0; i < 5; i++) {for (j = 0; j < 5; j++) {*(acc_data_virtual_base + i * 5 + j) = layer2poolout[inchannel][i][j];}}for (i = 0; i < ConvSize; i++) {for(j = 0; j < ConvSize; j++) {*(acc_data_virtual_base + FPGA_Weight + i * 5 + j) = conv_w3[outchannel][inchannel][i][j];}}if (inchannel == 0){accconvolution(5, 5, 1, 0);}else{accconvolution(5, 5, 1, 1);}}*(acc_data_virtual_base + FPGA_Weight) = conv_b3[outchannel];accbiasrelu(1, 1, 1, 0);layer3out[outchannel] = *(acc_data_virtual_base + FPGA_Result);}//----------------------------------------------//  第一层全连接//----------------------------------------------//全连接运算被拆分成16次乘累加的组合(单次全连接做16次乘累加运算)//总共NER个输出神经元INFO_TRACE("level 1 accfullconnect");for (ner = 0; ner < NER; ner++) {//初始化神经元neural_result[ner] = 0;//配置加速器//执行//输入layer3out[120], 120/16 = 7.5 ,需调用8次全连接算子for(j = 0; j < 8; j++){for ( i = 0;i < 16; i++){//超过图像大小的补0if((j == 7) && (i > 7)){*(acc_data_virtual_base + i) = 0.0;*(acc_data_virtual_base + FPGA_Weight + i) = 0.0;}else{*(acc_data_virtual_base + i) = layer3out[j * 16 + i];*(acc_data_virtual_base + FPGA_Weight + i) = fc_w1[ner][j * 16 + i];}}accfullconnect();//读取结果neural_result[ner] += *(acc_data_virtual_base + FPGA_Result);}//加偏置//此处channel循环已经结束//这里的加偏执由HPS完成neural_result[ner] += fc_b1[ner];}//----------------------------------------------//  第二层全连接//----------------------------------------------//  第二次全连接同样将运算拆分为16次乘累加的组合INFO_TRACE("level 2 accfullconnect");int outner, group, inner;//输出结果个数为ChrClsfor (outner = 0; outner < ChrCls; outner++) {//初始化输出结果数组probability[outner] = 0;//由于第一次全连接输出了84个神经元,因此拆分成6x16=96次运算for (group = 0; group < 6; group++) {//加载16次乘累加的数据与权重for (inner = 0; inner < 16; inner++) {if((group == 5) && (inner > 3)){*(acc_data_virtual_base + inner) = 0.0;*(acc_data_virtual_base + inner + FPGA_Weight) = 0.0;}else{*(acc_data_virtual_base + inner) = neural_result[inner + group * 16];*(acc_data_virtual_base + inner + FPGA_Weight) = fc_w2[outner][inner + group * 16];}}//执行accfullconnect();//读取结果并由HPS完成6次结果的累加probability[outner] += *(acc_data_virtual_base + FPGA_Result);}//加偏置probability[outner] += fc_b2[outner];}PROCESS_TRACE("hls_conv", "done");//----------------------------------------------//  得到最终结果//----------------------------------------------//找到结果数组中最大的值,其对应的下标就是识别结果int result = 0;float max = probability[0];for (i = 0; i < ChrCls; i++) {INFO_TRACE("probability_hls[%d]:%f", i, probability[i]);if (probability[i] > max) {max = probability[i];result = i;}}chrfind(result);VALUEs_TRACE("chrinfer", chrret);strcat(license, chrret);return (result);
}#undef _INFER_GLOBLE
#undef _PARA_GLOBLE

手写体数字识别设计具体实现步骤 

  • 打开提供好的黄金工程,将Verilog HDL代码设计源文件和C代码设计源文件放入指定文件夹中。如图6.2.1和6.2.2所示。

图6.2.1 HDL代码设计源文件

图6.2.2 C代码设计源文件

  • Windows命令窗口进入i++环境,将编写好的卷积神经推理算法C代码利用HLS高层次综合生成Quartus的IP工程文件。如图6.2.3和图6.2.4所示。

图6.2.3 i++环境

图6.2.4 生成Quartus工程

  • 将HLS生成的IP工程copy到黄金工程的IP目录下。如图6.2.5所示。

图6.2.5 复制工程

  • 进入Quartus工具内在platform  designer中进行IP布线。如图6.2.6、图6.2.7、图6.2.8、图6.2.9所示。

图6.2.6 打开platform designer

图6.2.7 布线

图6.2.8 分配基地址

图6.2.9 完成布线

  • 更新设备树烧入SD卡。如图6.2.10、图6.2.11、图6.2.12所示。

图6.2.10 生成.rbf文件

图6.2.11 生成.dtb文件

图6.2.12 拷贝到SD卡

  • 在SoC EDS工具中运行黄金工程generate_hps_qsys_header.sh文件更新IP生成后的保存基地址的头文件。如图6.2.13、图6.2.14、图6.2.15所示。

图6.2.12 .sh文件目录

图6.2.13 运行.sh文件更新头文件

图6.2.14 拷贝头文件到ps端工程中

  • 创建PS端工程。如图6.2.15、图6.2.16、图6.2.17、图6.2.18所示。

图6.2.15 打开eclipse软件

图6.2.16 创建gcc工程

图6.2.17 添加库文

图6.2.18 创建.c源文件

  • 编写PS端手写体算法控制模块设计,DDR储存模块设计等。如图6.2.19所示。

图6.2.19 PS端控制代码设计

  • 连接好电脑网口、开发板电源、摄像头、下载线。如图6.2.20所示。

图6.2.20 开发板接线图

  • 打开MobaXterm工具建立连接。如图6.2.21、图6.2.22、图6.2.23、图6.2.24、图6.2.25、图6.2.26、图6.2.27所示。

图6.2.21 适配连接

图6.2.22 MobaXterm工具

图6.2.23 配置参数

图6.2.24 进入操作界面

图6.2.25 添加驱动文件

图6.2.26 添加二进制主文件权限

图6.2.26 在PL端运行文件

图6.2.27 PL端运行结果

项目实现效果图

 总结

本文项目涉及到的知识和技术比较多也比较深,可能对于该阶段的作者本人来说想面面俱到很难,所以作者只是把推理算法模块、摄像头模块、项目数据流向以及整个项目框架了解清楚了,如有观点有错欢迎指正!

                           本项目基于重庆海云捷讯科技有限公司课程培训项目进行撰写,如侵权联系删除

这篇关于基于Cyclone V SoC利用HLS实现卷积手写体数字识别设计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/334638

相关文章

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

opencv图像处理之指纹验证的实现

《opencv图像处理之指纹验证的实现》本文主要介绍了opencv图像处理之指纹验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、简介二、具体案例实现1. 图像显示函数2. 指纹验证函数3. 主函数4、运行结果三、总结一、

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.