热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:二值图像连通域标记算法优化

本文由编程笔记#小编为大家整理,主要介绍了二值图像连通域标记算法优化相关的知识,希望对你有一定的参考价值。文章概要 非常感
本文由编程笔记#小编为大家整理,主要介绍了二值图像连通域标记算法优化相关的知识,希望对你有一定的参考价值。



文章概要

 

  非常感谢☆Ronny丶博主在其博文《图像分析:二值图像连通域标记》中对二值图像连通域的介绍和算法阐述,让我这个毫无数据结构算法底子的小白能够理解和复现代码。本文的目的是基于我自己的理解,对该博文中Two-Pass算法的一些优化和补充,同时也希望帮助更多像我一样的人较快地掌握连通域标记。

  连通域标记是图像分割计数的一个重要环节,在工业上应用非常地多。例如像硬币的计件,在二值化处理后,为了能够感知数量,就得对硬币区域进行标记(当然标记前可能还要经过一系列的形态学处理)。另外,还有一个我想到的,更有趣、也更具有挑战性的例子——二维码连通域标记,这用来检验算法的性能是再合适不过了。言归正题——本文介绍了两大流行算法,一个是利用DFS的Seed-Filling算法,另一个是Two-Pass算法。后者因为处理等价对的方法不同,又细分为DFS Two-Pass(使用DFS处理等价对)和Union-Find Two-Pass(使用并查集处理等价对)。如果硬要给这三种算法排序的话,大概是Union-Find Two-Pass > Seed-Filling > DFS Two-Pass,反正我写的程序是这样的速度排序

技术图片

 


Seed-Filling算法

 

  这个算法其实实质就是DFS,笔者曾经有幸做过一个“水洼连通”的算法题,当时就是用DFS或者BFS来做的,显然,“水洼连通”也是属于连通域标记问题的。DFS在这个问题上的思路是:优先地寻找一个完整连通域,在找的同时把他们都标记一下,找完一个完整连通域, 再去找下一个连通域。按照这个想法,程序无非就是维护一个堆栈或者队列罢了,写起来相对简洁易懂。要说缺点的话,就是频繁的堆栈操作可能会拉低程序的性能。

  简要地说明一下这部分代码含义,故事就定义成小明踩水坑吧,虽然小明对我表示自己很文静,只喜欢做数学题。首先,定义了一个二维矩阵labels,大小跟二值图一样。一开始labels都是标签0,这是一个无效标签,可以理解为是充满迷雾的未知区域或者是已确定的非水坑区域。小明每到达一个新的单位域(也就是一个新像素),首先要先看看这个域是不是未曾踩过的水坑(未曾踩过的水坑其标签为0且灰度值为255),如果是的话,那么小明就原地开心地踩水坑了,踩过以后还不忘给它画上一个大于0的标记(以标签1为例)。接下来,小明回顾四周,又发现了接壤的另一个水坑, 于是又在该水坑上留下了标记1······这样看似单调的循环,在小明眼里却是一次次奇妙的冒险。愉快的时光很短暂,小明不一会儿就发现身边已经没有“新鲜”的水坑了,伤心的同时回到最初的那个水坑,继续朝远方走去。渐渐地,眼前依稀出现了陌生又熟悉的水坑,重现微笑的小明决定要开启新的旅途,因此标记1.0进化至2.0。

  故事的结束,要额外补充一点,程序里要不停地将新的单位域加入队列, 因此队列遍历其上限是动态的。


1 vectorint>> seedFilling(Mat src)
2 {
3
4 // 标签容器,初始化为标记0
5 vectorint>> labels(src.rows, vector<int>(src.cols, 0));
6 // 当前的种子标签
7 int curLabel = 1;
8 // 四连通位置偏移
9 pair<int, int> offset[4] = {make_pair(0, 1), make_pair(1, 0), make_pair(-1, 0), make_pair(0, -1)};
10 // 当前连通域中的单位域队列
11 vectorint, int>> tempList;
12
13 for (int i = 0; i )
14 {
15 for (int j = 0; j )
16 {
17 // 当前单位域已被标记或者属于背景区域, 则跳过
18 if (labels[i][j] != 0 || src.at(i, j) == 0)
19 {
20 continue;
21 }
22 // 当前单位域未标记并且属于前景区域, 用种子为其标记
23 labels[i][j] = curLabel;
24 // 加入单位域队列
25 tempList.push_back(make_pair(i, j));
26
27 // 遍历单位域队列
28 for (int k = 0; k )
29 {
30 // 四连通范围内检查未标记的前景单位域
31 for (int m = 0; m <4; m++)
32 {
33 int row = offset[m].first + tempList[k].first;
34 int col = offset[m].second + tempList[k].second;
35 // 防止坐标溢出图像边界
36 row = (row <0) ? 0: ((row >= src.rows) ? (src.rows - 1): row);
37 col = (col <0) ? 0: ((col >= src.cols) ? (src.cols - 1): col);
38
39 // 邻近单位域未标记并且属于前景区域, 标记并加入队列
40 if (labels[row][col] == 0 && src.at(row, col) == 255)
41 {
42 labels[row][col] = curLabel;
43 tempList.push_back(make_pair(row, col));
44 }
45 }
46 }
47 // 一个完整连通域查找完毕,标签更新
48 curLabel++;
49 // 清空队列
50 tempList.clear();
51 }
52 }
53
54 return labels;
55 }

 


Two-Pass算法

 


等价对生成

 

  关于Two-Pass的算法原理可以参考上面提到的博文,原文还是很详细的,唯一的遗憾就是后面程序的注释有点少,看起来会吃力些,说白了就是自己菜。要找一张二维图像中的连通域,很容易想到可以一行一行先把子区域找出来,然后再拼合成一个完整的连通域,因为从每一行找连通域是一件很简单的事。这个过程中需要记录每一个子区域,为了满足定位要求,并且节省内存,我们需要记录子区域所在的行号、区域开始的位置、结束的位置,当然还有一个表征子区域总数的变量。需要注意的就是子区域开始位置和结束位置在行首和行末的情况要单独拿出来考虑。


1 // 查找每一行的子区域
2 // numberOfArea:子区域总数 stArea:子区域开始位置 enArea:子区域结束位置 rowArea:子区域所在行号
3 void searchArea(const Mat src, int &numberOfArea, vector<int> &stArea, vector<int> &enArea, vector<int> &rowArea)
4 {
5 for (int row = 0; row )
6 {
7 // 行指针
8 const uchar *rowData = src.ptr(row);
9
10 // 判断行首是否是子区域的开始点
11 if (rowData[0] == 255)
12 {
13 numberOfArea++;
14 stArea.push_back(0);
15 }
16
17 for (int col = 1; col )
18 {
19 // 子区域开始位置的判断:前像素为背景,当前像素是前景
20 if (rowData[col - 1] == 0 && rowData[col] == 255)
21 {
22 // 在开始位置更新区域总数、开始位置vector
23 numberOfArea++;
24 stArea.push_back(col);
25 // 子区域结束位置的判断:前像素是前景,当前像素是背景
26 }else if (rowData[col - 1] == 255 && rowData[col] == 0)
27 {
28 // 更新结束位置vector、行号vector
29 enArea.push_back(col - 1);
30 rowArea.push_back(row);
31 }
32 }
33 // 结束位置在行末
34 if (rowData[src.cols - 1] == 255)
35 {
36 enArea.push_back(src.cols - 1);
37 rowArea.push_back(row);
38 }
39 }
40 }

 

  另外一个比较棘手的问题,如何给这些子区域标号,使得同一个连通域有相同的标签值。我们给单独每一行的子区域标号区分是很容易的事, 关键是处理相邻行间的子区域关系(怎么判别两个子区域是连通的)。

 

技术图片

 

  主要思路:以四连通为例,在上图我们可以看出BE是属于同一个连通域,判断的依据是E的开始位置小于B的结束位置,并且E的结束地址大于B的开始地址;同理可以判断出EC属于同一个连通域,CF属于同一个连通域,因此可以推知BECF都属于同一个连通域。

  迭代策略:寻找E的相连区域时,对前一行的ABCD进行迭代,找到相连的有B和C,而D的开始地址已经大于了E的结束地址,此时就可以提前break掉,避免不必要的迭代操作;接下来迭代F的时候,由于有E留下来的基础,因此对上一行的迭代可以直接从C开始。另外,当前行之前的一行如果不存在子区域的话,那么当前行的所有子区域都可以直接赋新的标签,而不需要迭代上一行。

  标签策略:以上图为例,遍历第一行,A、B、C、D会分别得到标签1、2、3、4。到了第二行,检测到E与B相连,之前E的标签还是初始值0,因此给E赋上B的标签2;之后再次检测到C和E相连,由于E已经有了标签2,而C的标签为3,则保持E和C标签不变,将(2,3)作为等价对进行保存。同理,检测到F和C相连,且F标签还是初始值0,则为F标上3。如此对所有的子区域进行标号,最终可以得到一个等价对的列表。

  下面的代码实现了上述的过程。子区域用一维vector保存,没办法直接定位到某一行号的子区域,因此需要用curRow来记录当前的行,用firstAreaPrev记录前一行的第一个子区域在vector中的位置,用lastAreaPrev记录前一行的最后一个子区域在vector中的位置。在换行的时候,就去更新刚刚说的3个变量,其中firstAreaPrev的更新依赖于当前行的第一个子区域位置,所以还得用firstAreaCur记录当前行的第一个子区域。


1 // 初步标签,获取等价对
2 // labelOfArea:子区域标签值, equalLabels:等价标签对 offset:0为四连通,1为8连通
3 void markArea(int numberOfArea, vector<int> stArea, vector<int> enArea, vector<int> rowArea, vector<int> &labelOfArea, vectorint, int>> &equalLabels, int offset)
4 {
5 int label = 1;
6 // 当前所在行
7 int curRow = 0;
8 // 当前行的第一个子区域位置索引
9 int firstAreaCur = 0;
10 // 前一行的第一个子区域位置索引
11 int firstAreaPrev = 0;
12 // 前一行的最后一个子区域位置索引
13 int lastAreaPrev = 0;
14
15 // 初始化标签都为0
16 labelOfArea.assign(numberOfArea, 0);
17
18 // 遍历所有子区域并标记
19 for (int i = 0; i )
20 {
21 // 行切换时更新状态变量
22 if (curRow != rowArea[i])
23 {
24 curRow = rowArea[i];
25 firstAreaPrev = firstAreaCur;
26 lastAreaPrev = i - 1;
27 firstAreaCur = i;
28 }
29
30 // 相邻行不存在子区域
31 if (curRow != rowArea[firstAreaPrev] + 1)
32 {
33 labelOfArea[i] = label++;
34 continue;
35 }
36 // 对前一行进行迭代
37 for (int j = firstAreaPrev; j <= lastAreaPrev; j++)
38 {
39 // 判断是否相连
40 if (stArea[i] <= enArea[j] + offset && enArea[i] >= stArea[j] - offset)
41 {
42 if (labelOfArea[i] == 0)
43 // 之前没有标记过
44 labelOfArea[i] = labelOfArea[j];
45 else if (labelOfArea[i] != labelOfArea[j])
46 // 之前已经被标记,保存等价对
47 equalLabels.push_back(make_pair(labelOfArea[i], labelOfArea[j]));
48 }else if (enArea[i] offset)
49 {
50 // 为当前行下一个子区域缩小上一行的迭代范围
51 firstAreaPrev = max(firstAreaPrev, j - 1);
52 break;
53 }
54 }
55 // 与上一行不存在相连
56 if (labelOfArea[i] == 0)
57 {
58 labelOfArea[i] = label++;
59 }
60 }
61 }

 

 


DFS Two-Pass算法

 

  通过上面的努力,标记任务并没有做完,最核心的部分正是如何处理等价对。这里简单贴上原博主说的DSF方法,又是深搜啊。相比于直接DFS标记连通域,先找等价对再深搜减少了大量的堆栈操作,因为前者深度取决于连通域的大小,而后者是连通域数量,显然这两个数量级的差距挺大的。

  原博主的想法是建立一个Bool型等价对矩阵,用作深搜环境。具体做法是先获取最大的标签值maxLabel,然后生成一个$maxLabel*maxLabel$大小的二维矩阵,初始值为false;对于例如(1,3)这样的等价对,在矩阵的(0,2)和(2,0)处赋值true——要注意索引和标签值是相差1的。就这样把所有等价对都反映到矩阵上。

  深搜的目的在于建立一个标签的重映射。例如4、5、8是等价的标签,都重映射到标签2。最后重映射的效果就是标签最小为1,且依次递增,没有缺失和等价。深搜在这里就是优先地寻找一列等价的标签,例如一口气把4、5、8都找出来,然后给他们映射到标签2。程序也维护了一个队列,当标签在矩阵上值为true,而且没有被映射过,就加入到队列。

  当然不一定要建立一个二维等价矩阵,一般情况,等价对数量要比maxLabel来的小,所以也可以直接对等价对列表进行深搜,但无论采用怎样的深搜,其等价对处理的性能都不可能提高很多。


1 // 等价对处理,标签重映射
2 void replaceEqualMark(vector<int> &labelOfArea, vectorint, int>> equalLabels)
3 {
4 int maxLabel = *max_element(labelOfArea.begin(), labelOfArea.end());
5 // 等价标签矩阵,值为true表示这两个标签等价
6 vectorbool>> eqTab(maxLabel, vector<bool>(maxLabel, false));
7 // 将等价对信息转移到矩阵上
8 vectorint, int>>::iterator labPair;
9 for (labPair = equalLabels.begin(); labPair != equalLabels.end(); labPair++)
10 {
11 eqTab[labPair->first -1][labPair->second -1] = true;
12 eqTab[labPair->second -1][labPair->first -1] = true;
13 }
14 // 标签映射
15 vector<int> labelMap(maxLabel + 1, 0);
16 // 等价标签队列
17 vector<int> tempList;
18 // 当前使用的标签
19 int curLabel = 1;
20
21 for (int i = 1; i <= maxLabel; i++)
22 {
23 // 如果该标签已被映射,直接跳过
24 if (labelMap[i] != 0)
25 {
26 continue;
27 }
28
29
30 labelMap[i] = curLabel;
31 tempList.push_back(i);
32
33 for (int j = 0; j )
34 {
35 // 在所有标签中寻找与当前标签等价的标签
36 for (int k = 1; k <= maxLabel; k++)
37 {
38 // 等价且未访问
39 if (eqTab[tempList[j] - 1][k - 1] && labelMap[k] == 0)
40 {
41 labelMap[k] = curLabel;
42 tempList.push_back(k);
43 }
44 }
45 }
46
47 curLabel++;
48 tempList.clear();
49 }
50 // 根据映射修改标签
51 vector<int>::iterator label;
52 for (label = labelOfArea.begin(); label != labelOfArea.end(); label++)
53 {
54 *label = labelMap[*label];
55 }
56
57 }

 

 


Union-Find Two-Pass算法

 

  如果读者看到了这里,真的要感谢一下您的耐心。Two-Pass算法的代码要比直接深搜来得多,用不好甚至性能还远不如深搜。原博主在文中提及了可以用稀疏矩阵来处理等价对,奈何我较为愚钝,读者可以自研之。

  讲到等价对,实质是一种关系分类,因而联想到并查集。并查集方法在这个问题上显得非常合适,首先将等价对进行综合就是合并操作,标签重映射就是查询操作(并查集可以看做一种多对一映射)。并查集具体算法我就不唠叨了,毕竟不怎么打程序设计竞赛。不过,采用并查集的话,函数定义估计就满天飞了,这里我包装了一下,做成了类——实在是有点强迫症,其中等价对生成的函数方法跟上面的是一样的。

  网上有一些代码也实现了这个算法,但是有的牺牲了秩优化,合并时让树指向较小的根,个人认为这样做太不值了。所以为了解决这个,我在并查集映射后,又用labelReMap来进行第二次的映射,主要的步骤跟前面的差不多。

  然后,自己跑了一下这代码,不算画图标记的时间,效率要比上面的快四五倍左右,实时性上肯定是绰绰有余了。


1 class AreaMark
2 {
3 public:
4 AreaMark(const Mat src,int offset);
5 int getMarkedArea(vectorint>> &area);
6 void getMarkedImage(Mat &dst);
7
8 private:
9 Mat src;
10 int offset;
11 int numberOfArea=0;
12 vector<int> labelMap;
13 vector<int> labelRank;
14 vector<int> stArea;
15 vector<int> enArea;
16 vector<int> rowArea;
17 vector<int> labelOfArea;
18 vectorint, int>> equalLabels;
19
20 void markArea();
21 void searchArea();
22 void setInit(int n);
23 int findRoot(int label);
24 void unite(int labelA, int labelB);
25 void replaceEqualMark();
26 };
27
28 // 构造函数
29 // imageInput:输入待标记二值图像 offsetInput:0为四连通,1为八连通
30 AreaMark::AreaMark(Mat imageInput,int offsetInput)
31 {
32 src = imageInput;
33 offset = offsetInput;
34 }
35
36 // 获取颜色标记图片
37 void AreaMark::getMarkedImage(Mat &dst)
38 {
39 Mat img(src.rows, src.cols, CV_8UC3, CV_RGB(0, 0, 0));
40 cvtColor(img, dst, CV_RGB2HSV);
41
42 int maxLabel = *max_element(labelOfArea.begin(), labelOfArea.end());
43 vector hue;
44 for (int i = 1; i<= maxLabel; i++)
45 {
46 // 使用HSV模型生成可区分颜色
47 hue.push_back(uchar(180.0 * (i - 1) / (maxLabel + 1)));
48 }
49
50 for (int i = 0; i )
51 {
52 for (int j = stArea[i]; j <= enArea[i]; j++)
53 {
54 dst.at(rowArea[i], j)[0] = hue[labelOfArea[i]];
55 dst.at(rowArea[i], j)[1] = 255;
56 dst.at(rowArea[i], j)[2] = 255;
57 }
58 }
59
60 cvtColor(dst, dst, CV_HSV2BGR);
61 }
62
63 // 获取标记过的各行子区域
64 int AreaMark::getMarkedArea(vectorint>> &area)
65 {
66 searchArea();
67 markArea();
68 replaceEqualMark();
69 area.push_back(rowArea);
70 area.push_back(stArea);
71 area.push_back(enArea);
72 area.push_back(labelOfArea);
73 return numberOfArea;
74 }
75
76 void AreaMark::searchArea()
77 {
78 for (int row = 0; row )
79 {
80 // 行指针
81 const uchar *rowData = src.ptr(row);
82
83 // 判断行首是否是子区域的开始点
84 if (rowData[0] == 255)
85 {
86 numberOfArea++;
87 stArea.push_back(0);
88 }
89
90 for (int col = 1; col )
91 {
92 // 子区域开始位置的判断:前像素为背景,当前像素是前景
93 if (rowData[col - 1] == 0 && rowData[col] == 255)
94 {
95 // 在开始位置更新区域总数、开始位置vector
96 numberOfArea++;
97 stArea.push_back(col);
98 // 子区域结束位置的判断:前像素是前景,当前像素是背景
99 }else if (rowData[col - 1] == 255 && rowData[col] == 0)
100 {
101 // 更新结束位置vector、行号vector
102 enArea.push_back(col - 1);
103 rowArea.push_back(row);
104 }
105 }
106 // 结束位置在行末
107 if (rowData[src.cols - 1] == 255)
108 {
109 enArea.push_back(src.cols - 1);
110 rowArea.push_back(row);
111 }
112 }
113 }
114
115
116
117 void AreaMark::markArea()
118 {
119 int label = 1;
120 // 当前所在行
121 int curRow = 0;
122 // 当前行的第一个子区域位置索引
123 int firstAreaCur = 0;
124 // 前一行的第一个子区域位置索引
125 int firstAreaPrev = 0;
126 // 前一行的最后一个子区域位置索引
127 int lastAreaPrev = 0;
128
129 // 初始化标签都为0
130 labelOfArea.assign(numberOfArea, 0);
131
132 // 遍历所有子区域并标记
133 for (int i = 0; i )
134 {
135 // 行切换时更新状态变量
136 if (curRow != rowArea[i])
137 {
138 curRow = rowArea[i];
139 firstAreaPrev = firstAreaCur;
140 lastAreaPrev = i - 1;
141 firstAreaCur = i;
142 }
143
144 // 相邻行不存在子区域
145 if (curRow != rowArea[firstAreaPrev] + 1)
146 {
147 labelOfArea[i] = label++;
148 continue;
149 }
150 // 对前一行进行迭代
151 for (int j = firstAreaPrev; j <= lastAreaPrev; j++)
152 {
153 // 判断是否相连
154 if (stArea[i] <= enArea[j] + offset && enArea[i] >= stArea[j] - offset)
155 {
156 if (labelOfArea[i] == 0)
157 // 之前没有标记过
158 labelOfArea[i] = labelOfArea[j];
159 else if (labelOfArea[i] != labelOfArea[j])
160 // 之前已经被标记,保存等价对
161 equalLabels.push_back(make_pair(labelOfArea[i], labelOfArea[j]));
162 }else if (enArea[i] offset)
163 {
164 // 为当前行下一个子区域缩小上一行的迭代范围
165 firstAreaPrev = max(firstAreaPrev, j - 1);
166 break;
167 }
168 }
169 // 与上一行不存在相连
170 if (labelOfArea[i] == 0)
171 {
172 labelOfArea[i] = label++;
173 }
174 }
175 }
176
177
178 // 并查集初始化
179 void AreaMark::setInit(int n)
180 {
181 for (int i = 0; i <= n; i++)
182 {
183 labelMap.push_back(i);
184 labelRank.push_back(0);
185 }
186 }
187
188 // 查根
189 int AreaMark::findRoot(int label)
190 {
191 if (labelMap[label] == label)
192 {
193 return label;
194 }
195 else
196 {
197 //路径压缩优化
198 return labelMap[label] = findRoot(labelMap[label]);
199 }
200 }
201
202 // 合并
203 void AreaMark::unite(int labelA, int labelB)
204 {
205 labelA = findRoot(labelA);
206 labelB = findRoot(labelB);
207
208 if (labelA == labelB)
209 {
210 return;
211 }
212 // 秩优化,秩大的树合并秩小的树
213 if (labelRank[labelA] < labelRank[labelB])
214 {
215 labelMap[labelA] = labelB;
216 }
217 else
218 {
219 labelMap[labelB] = labelA;
220 if (labelRank[labelA] == labelRank[labelB])
221 {
222 labelRank[labelA]++;
223 }
224 }
225
226 }
227
228 // 等价对处理,标签重映射
229 void AreaMark::replaceEqualMark()
230 {
231 int maxLabel = *max_element(labelOfArea.begin(), labelOfArea.end());
232
233 setInit(maxLabel);
234
235 // 合并等价对,标签初映射
236 vectorint, int>>::iterator labPair;
237 for (labPair = equalLabels.begin(); labPair != equalLabels.end(); labPair++)
238 {
239 unite(labPair->first, labPair->second);
240 }
241
242 // 标签重映射,填补缺失标签
243 int newLabel=0;
244 vector<int> labelReMap(maxLabel + 1, 0);
245 vector<int>::iterator old;
246 for (old = labelMap.begin(); old != labelMap.end(); old++)
247 {
248 if (labelReMap[findRoot(*old)] == 0)
249 {
250 labelReMap[findRoot(*old)] = newLabel++;
251 }
252 }
253 // 根据重映射结果修改标签
254 vector<int>::iterator label;
255 for (label = labelOfArea.begin(); label != labelOfArea.end(); label++)
256 {
257 *label = labelReMap[findRoot(*label)];
258 }
259
260 }  

 

  最后的最后,这些代码都没有经历过“岁月的历练”,如果存在不合理之处,请读者指正!

 

  

 


推荐阅读
  • vue使用
    关键词: ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 利用空间换时间减少时间复杂度以及以C语言字符串处理为例减少空间复杂度
    在处理字符串的过程当中,通常情况下都会逐个遍历整个字符串数组,在多个字符串的处理中,处理不同,时间复杂度不同,这里通过利用空间换时间等不同方法,以字符串处理为例来讨论几种情况:1: ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文讨论了如何使用Web.Config进行自定义配置节的配置转换。作者提到,他将msbuild设置为详细模式,但转换却忽略了带有替换转换的自定义部分的存在。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
  • ZABBIX 3.0 配置监控NGINX性能【OK】
    1.在agent端查看配置:nginx-V查看编辑时是否加入状态监控模块:--with-http_stub_status_module--with-http_gzip_stat ... [详细]
author-avatar
只都是孩子Whitney
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有