GPU连通域分析方法

第1章连通域分析方法

连通域分析方法用于提取图像中相似属性的区域,并给出区域的面积,位置等特征信息。分为两种,基于游程(Runlength),和基于标记(Label)。

基于游程的方法,按照行对图像进行游程编码,然后,进行深度优先的逐区域扫描,或者广度优先逐行扫描的游程连接。一般来说广度优先扫描只需要扫描一遍游程即得到区域信息,效率较深度优先高。

基于标记的方法,使用一张标记图,将原始图中相似属性的区域,在标记图中赋值为相同的像素。再对标记图进行后处理,得到区域信息。
经过测试,基于游程的方案,不管是传输原始blob图到CPU进行游程提取与连接,还是在GPU中对blob图提取游程(压缩),只拷贝(压缩后)游程信息到CPU,其耗时都比较高。

在3060显卡上,只有当游程像素不超过全图的0.5%的情况下,才会比拷贝全图的方案有优势。在3090显卡上,只有当游程像素不超过全图的1.5%的情况下,才会比拷贝全图的方案有优势。

因此调研了在GPU上进行连通域分析的方法,认为基于标记的方法更适合在GPU上运行,在GPU上直接得到Blob缺陷信息。

基于标记的Komura方法,当前未优化代码,8k8k图像的区域标记在Nvidia的RTX4080上,仅需要3ms,Playne方法仅需要2.4ms。标记压缩耗时当前可以优化到11ms,区域信息提取并拷贝到CPU,需要1ms。相当于15ms内就完成了8k8k图像的blob分析。而基于游程的方法,仅从GPU拷贝8k8k图像的理论耗时就耗时7ms(64M/(12.8G/s70%)),即使采用GPU游程压缩方法,游程压缩步骤耗时也在7ms。

1.1.基于游程的方法

首先对图像按行进行游程编码,然后进行游程连接。

1.1.1.游程编码

在这里插入图片描述
描述方法为:(x1,x2,y),x采用左闭右开区间。(1,6,1),(2,5,2)。采用2个三元组即描述了如上的区域。
该算法逐个读取像素,和阈值进行比较,实际执行效率极其低下。以下为采用SIMD并行指令集优化后的代码片段。
核心的指令共四条:alignr,xor,cmp1,cmp2。

	mres = _mm_loadu_si128((const __m128i*)(pBlob1-16))//0-15m1 = _mm_loadu_si128((const __m128i*)pBlob1); //16-31mOffset = _mm_alignr_epi8(m1, mres, 1);  //1-16//mH, mL 作差, 一个非0,一个是0,则对应位置赋值为1mR = _mm_xor_si128(_mm_cmpeq_epi8(mres, mzero), _mm_cmpeq_epi8(mOffset, mzero));auto a = _pext_u64(_pos, mR.m128i_u64[0]);while ((uint8_t)a) { pRun1[++n1Count] = x * 16 + uint8_t(a); a = a >> 8; }auto b = _pext_u64(_pos, mR.m128i_u64[1]);while ((uint8_t)b) { pRun1[++n1Count] = x * 16 + 8 + uint8_t(b); b = b >> 8; }

alignr实现像素的错位,根据0-15和16-31,生成1-16。
0-15和阈值比较,得到二值,1-16和阈值比较,得到二值。
0-15的二值和1-16的二值异或 xor,结果为1的像素必定为游程的边界。
使用_pext_u64搜集1的位置,得到游程编码。

1.1.2.游程连接

深度优先的方法实现检测,代码容易理解。而广度优先的方法,为了得到极致的效率,采用链表,其数据结构设计通常很复杂。
采用分块的方式,每块放8个游程,块间用链表连接,放满后放到下一个块。
当一个blob有7个游程时:
在这里插入图片描述

当一个blob有10个游程时:
在这里插入图片描述

红色的是索引指针,永远指向链表的尾部。绿色是next指针。
当一个blob有18个游程时:
在这里插入图片描述

红色的是索引指针,永远指向链表的尾部。绿色是next指针。
可以看到,除了正常的单向链表连接外,设计了从首块到尾块的连接,使用首块的地址作为blob的唯一标识,如果判断新的游程需要加入这个blob,则从首块直接定位到可以加入未满的块。
在这里插入图片描述
如上图所示,如果两个游程被下面的一行连接到一起,则需要对两个独立的链表进行连接,连接方法为:
当一个有18个游程和一个有20个游程的blob被连到一起后:
在这里插入图片描述
蓝色为B,红色为A。
将B的尾(B的头索引指针指向位置)的next指针指向A的头。
将B的头的索引指针指向A的尾(A的头索引指针指向位置)(新的游程将插入这个分块)。

1.1.3.执行结果

算法执行结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.1.4.总结:基于游程的方法是适合CPU的方法

基于游程的方法,不管是游程提取还是游程连接,都适合在CPU完成。
比如游程提取步骤,换到GPU上,每行开启一个线程进行游程提取,其耗时已经超过了从GPU拷贝整张blob图像到CPU的时间。
至于游程连接步骤,则更是不可能在GPU上实现。

1.2.基于标记的方法

首先进行像素标记,然后对标记进行处理得到区域信息。
在这里插入图片描述

1.2.1.像素标记

将灰度值接近,或者同类像素的对应位置的标记像素设置为相同值。在CPU上采用递归实现。

// 深度优先搜索函数,用于连通域分析和像素标记
void DFS(uint8_t* image, int* labels, int currentLabel, int x, int y, int nW, int nH) 
{if (x < 0 || x >= nW || y < 0 || y >= nH || labels[y * nW + x] != 0 || image[y * nW + x] == 0) 		return;  // 递归终止条件// 标记当前像素labels[y * nW + x] = currentLabel;// 遍历相邻的像素for (int i = -1; i <= 1; ++i) for (int j = -1; j <= 1; ++j) dfs(image, labels, currentLabel, x + i, y + j, nW, nH);
}// 像素标记
void Labeling(uint8_t* image, int* labels, int nW, int nH) 
{int currentLabel = 0;for (int i = 0; i < nH; ++i) {for (int j = 0; j < nW; ++j) {if (image[i * nW + j] == 1 && labels[i * nW + j] == 0) {// 新的连通域开始,递增标签值currentLabel++;DFS(image, labels, currentLabel, j, i, nW, nH);}}}
}

1.2.2.标记压缩(可选)

在GPU上进行区域信息获取时,需要知道共有多少个区域,或者哪些像素属于同一个区域,在计算特征时,属于一个区域的一堆像素只需要计算一个。
如果在标记阶段,采用深度优先的方法,则标记本身就是连续的,不需要该步骤。
但在GPU的像素标记实现算法中,通常采用合并的方式。因此,标记值必定不是从0到区域数量之间连续的,标记值最大可能是图像的像素个数。
此时需要对标记进行压缩,示意如下:
在这里插入图片描述
将之前不连续的前景标记修改为1的等差数列,在CPU上实现如下。

void compressLabels(int* labels, int width, int height)
{std::vector<int> uniqueLabels;std::unordered_map<int, int> labelMap;// 1. 找到所有不同的标签值for (int i = 0; i < width * height; ++i) {int label = labels[i];if (label > 0 && std::find(uniqueLabels.begin(), uniqueLabels.end(), label) == uniqueLabels.end()){uniqueLabels.push_back(label);}}// 2. 为每个不同的标签值分配一个新的连续的整数值int newLabel = 0;for (int label : uniqueLabels) {labelMap[label] = newLabel++;}// 3. 遍历标签图,将每个像素的标签值替换为其新的整数值for (int i = 0; i < width * height; ++i){int label = labels[i];if (label > 0) {labels[i] = labelMap[label];}}
}

1.2.3.区域信息提取

遍历图像,根据标记值,累加更新对应区域的面积,最大最小更新上下左右4个边界。

	std::vector<int> labelAreas(labelCount, 0);std::vector<int> labelTop(labelCount, nH);std::vector<int> labelLeft(labelCount, nW);std::vector<int> labelBottom(labelCount, 0);std::vector<int> labelRight(labelCount, 0);for (int i = 0; i < nH; ++i){for (int j = 0; j < nW; ++j) {int label = labels[i * nW + j];if (label > 0) {labelAreas[label]++;labelTop[label] = std::min(labelTop[label], i);labelLeft[label] = std::min(labelLeft[label], j);labelBottom[label] = std::max(labelBottom[label], i);labelRight[label] = std::max(labelRight[label], j);}}}

第2章基于标记的方法在GPU实现

1.2节中对基于标记的方式在CPU上的实现进行了原理验证。在GPU上的实现,需要采用不同的算法思路。

2.1.像素标记方法
为了充分利用GPU的并行处理优势,每像素一个线程,进行像素标记。

2.1.1.方案1:Union-Find 方法,2017年提出,Npp使用的方案

实现原理:An Optimized Union-Find Algorithm for Connected Components Labeling Using GPUs (2017)。
Step1:划分为3232的网格,对应1个block最多1024线程。
Step2:每个网格中标记初始化。
在这里插入图片描述
Step3:每个网格进行行列粗合并。
在这里插入图片描述
Step3:每个网格局部找根节点并修改标记为根节点标记值。
在这里插入图片描述
Step4:所有网格边界像素分析。
在这里插入图片描述
直接调用Nppi接口:
status = nppiLabelMarkersUF_8u32u_C1R(
(Npp8u
)imSrc.Ptr(), imSrc.Width(),
(Npp32u*)imAResult.Ptr(), imAResult.Width() * imAResult.Depth(),
oSrcSizeROI, nppiNormInf, d_tmp);
对于8k8k大小的图像,RTX4080耗时17ms。
在这里插入图片描述
和论文中耗时还有差距(4k
4k 1070耗时3.3ms)。
调用Npp,4080显卡,8k*8k图像需要17ms。自己实现可以超过Npp速度。

2.1.2.方案2:Komura方法,2015年提出

Komura亮点:在标记初始化阶段,进行了连接判断。得到一个区域内部像素串。根据像素串找根节点,将所有像素赋值为根节点的标记。
实现原理:GPU-based cluster-labeling algorithm without the use of conventional iteration use of conventional iteration(Komura,小村,2015)
Step1初始化:标记初始化,每个像素如果和左上有连接,初始化为左上像素的标记值。
Step2分析:每个像素找左上,递归找,直到找到根节点,称为root,当前像素赋值为根节点。
Step3合并:标记合并,一个区域多个root的情况消除。
Step4分析:同Step2,重新找根节点。
在这里插入图片描述
Step5:背景像素标记赋值为0。
该论文中的直接法,有github实现
Github地址:https://github.com/FolkeV/CUDA_CCL
对于8k*8k大小的图像,4080显卡,耗时3.5ms。
论文中给出了Komura直接法和两阶段法的耗时对比
在这里插入图片描述
采用两阶段方法,相比直接法,有10%的效率提升。

2.1.3.方案3:Playne方法,2018年提出

实现原理:A New Algorithm for Parallel Connected-Component Labelling on GPUs(Playne 普莱恩 2018)。
论文中对比了label方法,Komura方法和作者本人提出的Playne方法。
Komura方法和Playne方法比较:
在这里插入图片描述
Playne使用两阶段,block,共享内存和shl指令优化。
Playne方法优化后(相比Komura直接法60%左右的提升)
对于Ising数据集,,Playne方法相比Komura有66%的效率提升(40ms->24ms)。
作者使用显卡为K20X,处理能力为3060的1/4,为3090的1/8。即对于8k*8k的图像,完成区域标记仅需要3ms。
Playne方法论文中使用的K20显卡和3060显卡比较

2.2.标记压缩方法

将之前不连续的前景标记修改为1的等差数列,如下图所示:
在这里插入图片描述

2.2.1.方案1:串行得到压缩后映射关系,耗时6000ms

GPU串行方案,使用1个线程计算压缩前标记和压缩后标记的映射关系,使用像素个数线程修改标记。
1个线程的核函数,8k*8k大小的图像,运行需要6200ms。

__global__ void compressLabelsGetMax(int* labels, int* labelMap, int* nCount,int width, int height)
{bool isFind;for (int i = 0; i < width * height; ++i){int label = labels[i];if (label <= 0) continue;isFind = false;for (int j = 0; j < *nCount; j++){if (labelMap[j] == label){isFind = true;break;}}if (!isFind){labelMap[(*nCount)++] = label;}}
}

而得到映射关系后,使用像素个数线程修改标记的核函数,运行时间为4ms,核函数实现在第2章2.3小节。

2.2.2.方案2:排序+去重,当前最优实现12ms

拷贝图像,所有像素排序,去掉相邻的重复像素。

//图像拷贝0.9ms
cudaMemcpy(d_labels_cpy, d_labels, numPixels * sizeof(int), cudaMemcpyDeviceToDevice);//数据结构转换
device_ptr<uint32_t> dev_ptr(d_labels_cpy);
device_vector<uint32_t> d_vector(dev_ptr, dev_ptr + nW * nH);//GPU排序10ms
GPU_sort(d_vector.begin(), d_vector.end());//相邻的重复像素去重 0.6ms
auto uni = GPU_unique(d_vector.begin(), d_vector.end());//blob数量
nCompressedLabelCount = uni - d_vector.begin();
std::cout << nCompressedLabelCount << std::endl;

拷贝图像0.9ms,像素排序10ms,去掉相邻的重复像素0.6ms,共耗时11.5ms。
逐像素映射的核函数耗时0.5ms,共耗时12ms。
该方法在stackoverflow也查到过:
https://stackoverflow.com/questions/28797147/compress-sparse-data-with-cuda-ccl-connected-component-labeling-reduction
在这里插入图片描述
另外提供了一种使用thrust::lower_bound代替二分查找的方案。
二分查找的方案:
https://stackoverflow.com/questions/21658518/search-an-ordered-array-in-a-cuda-kernel

2.2.3.方案3:直接调用Nppi提供的接口

nppi中提供的接口,对标记图进行原地操作,直接得到压缩后的图像;

status = nppiCompressMarkerLabelsUF_32u_C1IR((Npp32u*)d_labels, numCols * 4, oSrcSizeROI,nW * nH, &nCompressedLabelCount, d_tmp2);  //12ms

总耗时12ms,和方案2耗时接近。因此猜测nppi中原理同方案2。

2.3.标记映射方法

根据2.2.1和2.2.2中得到的映射表,修改标记图像素。核函数实现,对于8k*8k大小的图像,运行需要4ms:

__global__ void compressLabelsRemap(int* labels, int* labelMap, int* nCount, int width, int height)
{int x = blockIdx.x * blockDim.x + threadIdx.x;int y = blockIdx.y * blockDim.y + threadIdx.y;int i = 0;if (x < width && y < height) {int index = y * width + x;int label = labels[index];bool isFind = false;for (i = 0; i < *nCount; i++){if (label == labelMap[i]){isFind = true;break;}}if (!isFind)return;labels[index] = i;}
}

需要对于8k*8k大小的图像,需要4ms。
https://stackoverflow.com/questions/21658518/search-an-ordered-array-in-a-cuda-kernel
这个可以优化,参考这个链接,使用二分搜索,有6倍的效率提升,可做到1ms以下。

2.4.区域信息提取方法

根据1.2.3中CPU上的代码中for循环内容放到核函数,一个线程计算一个像素,最终得到,8k*8k图像,允许耗时0.4ms。

__global__ void compute_blob_info(unsigned int* labels, int* labelAreas, int* labelTop, int* labelLeft, int* labelBottom, int* labelRight, int labelCount, int width, int height)
{int x = blockIdx.x * blockDim.x + threadIdx.x;int y = blockIdx.y * blockDim.y + threadIdx.y;if (x >= width && y >= height)return;int label = labels[y * width + x] - 1;if (label >= labelCount || label <= 0)return;atomicAdd(&labelAreas[label], 1);atomicMin(&labelTop[label], y);atomicMin(&labelLeft[label], x);atomicMax(&labelBottom[label], y);atomicMax(&labelRight[label], x);
}

2.5.存在问题及解决方案

基于标记的方案,暂时可以拿到缺陷的外接矩形和面积。缺陷的对比度,可以在GPU中计算,缺陷的区域,如果支持GPU图像直接显示,也可以在界面展示。
标记图采用Int32 最大支持4k4k的图像。 可以采用分块的形式接触限制。
标记压缩步骤当前还没有找到比较好的优化方案,当前最优实现方案和nppi的耗时相当,8k
8k的图像需要12ms。

第3章基于游程的方法在GPU实现

1.2节中对基于标记的方式在CPU上的实现进行了原理验证。在GPU上的实现,需要采用不同的算法思路。
方案1:将blob全图从GPU拷贝到CPU,在CPU中完成游程提取。
方案2:在GPU中完成游程提取,进行压缩后,拷贝到CPU。
如下图所示,对图中的游程区域进行提取并压缩,增加图中游程区域的数据量,进行耗时统计,图像大小为8K*8K,按游程区域占比为1%,2%,4%,100%进行统计。
4%前景像素blob图像8k*8k
100%前景像素blob图像8k*8k

3.1.游程提取

游程提取完成如下操作:
在这里插入图片描述
使用一张游程图像,记录每一行前景区域的左右边界。每行开启一个线程,进行游程提取,统计游程的占比对游程提取耗时的影响。在4080显卡上进行测试,图像大小为8K*8K。

ms1%2%4%100%
游程提取1.31.31.30.6

不管提多少数据量的游程,耗时无变化。
100%游程图像8k*8k,由于有规律所以耗时短
100%游程图像8k*8k,由于有规律所以耗时短。

template<bool bLight>
__global__ void GetRun(uint8_t* d_blob, uint16_t* d_run, int nRunStep, uint32_t* d_count, int nHeight, int nWidth,int nThre)
{const unsigned int iy = (blockIdx.x * blockDim.x) + threadIdx.x;if (iy >= nHeight)return;uint8_t* pLine = d_blob + iy * nWidth;uint16_t* pRunLine = d_run + nRunStep / sizeof(uint16_t) * iy;uint16_t nRun = 1;uint16_t nRunCount = 0;bool bFlag = false;int ix;for (ix = 0; ix < nWidth; ix++){if (bLight){if (pLine[ix] > nThre){if (!bFlag){pRunLine[nRun++] = ix;bFlag = true;}nRunCount++;}else{if (bFlag){pRunLine[nRun++] = ix;bFlag = false;if (nRun / 2 > 499)break;}}}else{if (pLine[ix] < nThre){if (!bFlag){pRunLine[nRun++] = ix;bFlag = true;}nRunCount++;}else{if (bFlag){pRunLine[nRun++] = ix;bFlag = false;if (nRun / 2 > 499)break;}}}}if (bFlag){pRunLine[nRun++] = ix;}pRunLine[0] = nRun - 1;d_count[iy] = nRun;//if (iy == 0)//{//	for (int i = 0; i < nRun; i++)//		printf("%d ", pRunLine[i]);//	printf("\n");//}
}

这个实现不一定是最优的,亮暗同时提取,应该能实现更快的速度。

GetRun<true> << <grid, block >> >  (d_blob, d_run, nRunStep, d_count, nH, nW, 128+6);
GetRun<false> << <grid, block >> > (d_blob, d_run + nRunStep/sizeof(uint16_t) * nH, nRunStep, d_count, nH, nW, 128 - 6);

提取结果:
100%游程图像游程提取结果

3.2.游程压缩

游程压缩做如下操作:
在这里插入图片描述
将每一行稀疏的游程,拷贝到一起,便于拷贝传输。为了实现并行拷贝,需要知道每一行存放的起始位置,这个操作就是前缀和。
在这里插入图片描述
首先计算绿色数组,由于要存放游程个数,因此,每个元素加1。
然后计算蓝色数组,每个元素是绿色数组中下标小于当前下标的所有像素和。
如果采用如下的核函数计算,则不能充分利用CPU的并行计算能力:

__global__ void GetRun(uint32_t* d_count, uint32_t* d_presum, int nLength)
{const unsigned int ix = (blockIdx.x * blockDim.x) + threadIdx.x;if (ix >= nLength)return;for (int i = 0; i < ix; i++)d_presum[ix] = d_count[ix];
}

采用GPU前缀和优化方法,当前采用thrust中接口:

thrust::exclusive_scan(thrust::device, d_count, d_count + nHRun, d_presum);  //0.04

则耗时可以忽略不计。
然后每行开启一个线程,进行游程数据拷贝,最后一行线程计算压缩后数据长度:

__global__ void copy_height(uint16_t * pRun, int nRunStep, uint32_t * d_count, uint32_t * d_presum, int nHeight, uint16_t * pCompressRun, int* pnCompressLenght)
{const unsigned int iy = (blockIdx.x * blockDim.x) + threadIdx.x;if (iy >= nHeight)return;memcpy(pCompressRun + d_presum[iy],   //TopRun + nRunStep / sizeof(uint16_t) * iy, //From(d_count[iy]) * sizeof(uint16_t)); //Countif (iy == nHeight - 1){*pnCompressLenght = d_presum[iy] + d_count[iy] + 1;}
}

核函数的调用方法如下:

dim3 block1(16);
dim3 grid1((nHRun + 15) / 16);
copy_height << < grid1, block1 >> > (d_run, nRunStep, d_count, d_presum, nHRun, d_pCompressRun, pnCompressLenght);

在4080显卡上进行测试,图像大小为8K*8K。

ms1%2%4%100%
前缀和计算0.040.040.040.04
游程压缩0.040.050.070.6
随着提取游程数量的增加,压缩的耗时也会增加,但总体耗时不超过1ms。

3.3.游程拷贝

将压缩后的游程从GPU拷贝到CPU。
对于一张8K*8K的图,3060显卡,GPU拷贝到CPU上耗时8ms,下表统计不同游程数据压缩后的拷贝耗时,相比于blob全图拷贝,有多少提升,单位为ms。
100%前景像素blob图像,每行最多提取500个游程。

ms1%2%4%100%
压缩数据拷贝0.070.10.140.54
占全图拷贝%0.8%1.25%1.7%6%

3.4.效率比较

方案1:GPU拷贝完整blob图到CPU,在CPU中完成游程提取。
方案2:GPU进行游程提取,并进行游程压缩,将压缩后游程拷贝到CPU。
100%游程像素下(每行最多取500个游程)
100%游程像素下(每行最多取500个游程)
100%游程像素示意(每行最多取500个游程)

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

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

相关文章

3D Gaussian Splatting复现

最近3D Gaussian Splatting很火&#xff0c;网上有很多复现过程&#xff0c;大部分都是在Windows上的。Linux上配置环境会方便简单一点&#xff0c;这里记录一下我在Linux上复现的过程。 Windows下的环境配置和编译&#xff0c;建议看这个up主的视频配置&#xff0c;讲解的很细…

[算法与数据结构][python]:Python参数传递,“值传递”还是“引用传递“?

Python中的函数参数传递方式是“传对象引用”&#xff0c;可以理解为“值传递”和“引用传递”的混合体。 在Python中&#xff0c;所有的数据类型都是对象。如果函数参数是不可变对象&#xff08;如整数、字符串、元组&#xff09;&#xff0c;那么传递的就是对象的值&#xf…

695岛屿最大面积

题目 给定一个 row x col 的二维网格地图 grid &#xff0c;其中&#xff1a;grid[i][j] 1 表示陆地&#xff0c; grid[i][j] 0 表示水域。 网格中的格子 水平和垂直 方向相连&#xff08;对角线方向不相连&#xff09;。整个网格被水完全包围&#xff0c;但其中恰好有一个…

逻辑回归简单案例分析--鸢尾花数据集

文章目录 1. IRIS数据集介绍2. 具体步骤2.1 手动将数据转化为numpy矩阵2.1.1 从csv文件数据构建Numpy数据2.1.2 模型的搭建与训练2.1.3 分类器评估2.1.4 分类器的分类报告总结2.1.5 用交叉验证&#xff08;Cross Validation&#xff09;来验证分类器性能2.1.6 完整代码&#xf…

malloc calloc 与 realloc

malloc 原型 void *malloc(size_t size);size字节为单位&#xff0c;保持原数据&#xff0c;不做初始化。 calloc 原型 void *calloc(size_t n, size_t size);分配n*size 字节数 初始化为零。 realloc 原型 void *realloc (void *ptr, size_t size)扩容&#xff0c;重新分配…

JavaWeb 里的Vue,Springboot,Mvc,Servlet,JSP,SSM都是什么?

在Java Web开发中&#xff0c;使用一系列技术和框架可以构建强大、高效的Web应用程序。在这个领域&#xff0c;一些关键的技术包括Vue.js、Spring Boot、MVC、Servlet、JSP以及SSM&#xff08;Spring Spring MVC MyBatis&#xff09;。本文将对这些技术进行详细解释&#xff…

copilot插件全解

COPILOT是一个基于AI的编程辅助工具&#xff0c;它可以帮助程序员自动编写代码&#xff0c;提高开发效率。COPILOT的插件主要是为了将其功能集成到不同的编程环境中&#xff0c;方便程序员使用。 目前&#xff0c;COPILOT支持多种编程环境&#xff0c;包括Visual Studio Code、…

钉钉审批流程解读

组织机构 部门 部门可以创建下级部门部门可以设置部门主管&#xff0c;可以是多人部门可以默认构建&#xff0c;沟通群可以设置部门信息&#xff0c;比如电话、简介可以设置部门的可见性&#xff0c;比如隐藏本部门&#xff0c;本部门将不会在组织机构、搜索&#xff0c;个人…

怎么让视频进行加速处理并保存

要加速处理视频并保存&#xff0c;可以使用专业的视频编辑软件或者一些在线工具。以下是一种常见的方法&#xff0c;使用FFmpeg这个开源工具。请确保你已经安装了FFmpeg。 打开命令行界面&#xff1a; 打开终端或命令提示符窗口。 使用以下命令进行视频加速处理&#xff1a; f…

如何从格式化的 Windows 和 Mac 电脑硬盘恢复文件

格式化硬盘可为您提供全新的体验。它可以是硬盘驱动器定期维护的一部分&#xff0c;是清除不再使用的文件的一种方法&#xff0c;在某些情况下&#xff0c;它是处理逻辑损坏的万福玛利亚。但是&#xff0c;许多用户发现自己格式化了错误的分区或驱动器&#xff0c;或者后来意识…

c语言-指针进阶

文章目录 前言一、字符指针二、数组指针2.1 数组指针基础2.2 数组指针作函数参数 总结 前言 在c语言基础已经介绍过关于指针的概念和基本使用&#xff0c;本篇文章进一步介绍c语言中关于指针的应用。 一、字符指针 字符指针是指向字符的指针。 结果分析&#xff1a; "ab…

Java中常见的设计模式及其实际应用

在软件开发中&#xff0c;设计模式是重要的指导原则&#xff0c;它们提供了解决特定问题的可重用方案。Java作为一种强大的编程语言&#xff0c;广泛应用了许多设计模式。让我们深入探讨几种常见的设计模式&#xff0c;并展示它们在实际Java应用中的用例。 1. 单例模式 (Singl…

elementui loading自定义图标和字体样式

需求&#xff1a;页面是用了很多个loading&#xff0c;需要其中有一个字体大些&#xff08;具体到图标也一样的方法&#xff0c;换下类名就行&#xff09; 遇见的问题&#xff1a;改不好的话会影响其他的loading样式&#xff08;一起改变了&#xff09; 效果展示 改之前 改之…

使用conda管理Python虚拟环境

标题&#xff1a;使用conda管理Python虚拟环境 摘要&#xff1a;本文将介绍如何使用conda工具创建、查看和删除Python虚拟环境。通过使用conda&#xff0c;我们可以轻松地在不同的项目中使用不同的Python版本和依赖库&#xff0c;避免不同项目之间的冲突。 一、简介 Python是…

使用 Kafka 和 CDC 将数据从 MongoDB Atlas 流式传输到 SingleStore Kai

SingleStore 提供了变更数据捕获 (CDC) 解决方案&#xff0c;可将数据从 MongoDB 流式传输到 SingleStore Kai。在本文中&#xff0c;我们将了解如何将 Apache Kafka 代理连接到 MongoDB Atlas&#xff0c;然后使用 CDC 解决方案将数据从 MongoDB Atlas 流式传输到 SingleStore…

IDEA好用插件

CodeGlance Pro 右侧代码小地图 Git Commit Template git提交信息模板 IDE Eval Reset 无限试用IDEA Maven Helper 图形化展示Maven项 One Dark theme 好看的主题 SequenceDiagram 展示方法调用链 Squaretest 生成单元测试 Translation 翻译 Lombok lombok插件…

【开题报告】基于JavaWeb的年货销售系统的设计与实现

1.选题背景 年货销售是中国传统文化的一部分&#xff0c;也是中国人过年必备的习俗之一。随着互联网的发展&#xff0c;越来越多的人选择在网上购买年货&#xff0c;以节省时间和精力。为了满足人们对年货的购买需求&#xff0c;设计一个基于JavaWeb的年货销售系统具有重要意义…

leecode | 829连续整数求和

给一个整数n 求连续整数的和等于n 的个数 这道题 是一个数论的思想 解决思路&#xff1a; 数必须是连续的&#xff0c;可以转化成一个通用的公式&#xff0c;以101为例做一般性推导&#xff0c;&#xff1a; 101 &#xff1d; 101 &#xff1d; 50 &#xff0b; 51 &#xff1d…

AQS原来是这么设计的,泰裤辣!

缘起 每门编程语言基本都离不开并发问题&#xff0c;Java亦如此。谈到Java的并发就离不开Doug lea老爷子贡献的juc包&#xff0c;而AQS又是juc里面的佼佼者 因此今天就一起来聊聊AQS 概念 AQS是什么&#xff0c;这里借用官方的话 Provides a framework for implementing blo…

web3: 智能合约

目录 智能合约的历史什么是智能合约如何运作?智能合约的应用代币标准ERC-20什么是 ERC-20?功能ERC-20 代币接口ERC-721什么是 ERC-721?功能ERC-721 代币接口:ERC-165ERC-777什么是 ERC-777&